diff --git a/README.md b/README.md index 28ef47f..a40eaf4 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,15 @@ ### **_진짜 독서가들의 독서법, 북스테어즈 💡_** -북스테어즈는 여러분들의 보다 똑똑한 독서를 돕습니다! [랜딩페이지 바로가기](https://bookstairs.netlify.app/) +북스테어즈는 여러분들의 보다 똑똑한 독서를 돕습니다!
+[북스테어즈 바로가기](https://book-stairs.com/)
SOPT 29th APPJAM -- 프로젝트 기간: 2022.12.18 ~ +- 프로젝트 기간: 2021.12.18 ~ +- 릴리즈: 2022.03.17 ~ - [Notion](https://rose-prepared-583.notion.site/d454c4437530405f9e526e86e66912b3) - [API 명세서](https://rose-prepared-583.notion.site/API-42e1ea2497d344399fda98cfbe55febd) - [코드 컨벤션](https://rose-prepared-583.notion.site/Code-Convention-27afc41450b74fd8a5f1688bfb0b2ede) @@ -32,30 +34,7 @@ SOPT 29th APPJAM ![badge](https://img.shields.io/badge/Part-Back--end-brightgreen) ![lang](https://img.shields.io/badge/Language-TypeScript-blue) ![react](https://img.shields.io/badge/Tech--stack-Node.js-orange) ![realease](https://img.shields.io/badge/release-v1.0.0-yellow) -```json -"dependencies": { - "@types/express-serve-static-core": "^4.17.28", - "aws-sdk": "^2.1057.0", - "axios": "^0.24.0", - "bcryptjs": "^2.4.3", - "cors": "^2.8.5", - "dotenv": "^8.6.0", - "express": "^4.17.1", - "express-validator": "^6.10.0", - "gravatar": "^1.8.1", - "jsonwebtoken": "^8.5.1", - "moment": "^2.29.1", - "multer": "^1.4.2", - "multer-s3": "^2.9.0", - "pg": "^8.7.1", - "pg-hstore": "^2.3.4", - "reflect-metadata": "^0.1.13", - "request": "^2.88.2", - "sequelize": "^6.13.0", - "sequelize-cli": "^6.3.0", - "sequelize-typescript": "^2.1.0" -} -``` +
@@ -76,26 +55,31 @@ SOPT 29th APPJAM ``` 📦src ┣ 📂config - ┃ ┣ 📜config.ts ┃ ┗ 📜index.ts ┣ 📂controller ┃ ┣ 📜auth.ts ┃ ┣ 📜book.ts ┃ ┣ 📜review.ts ┃ ┗ 📜user.ts + ┣ 📂interface + ┃ ┣ 📜IBook.ts + ┃ ┣ 📜IReview.ts + ┃ ┗ 📜IUser.ts ┣ 📂library ┃ ┣ 📜checkValidation.ts ┃ ┣ 📜constant.ts + ┃ ┣ 📜convertSnakeToCamel.ts ┃ ┣ 📜response.ts ┃ ┗ 📜returnCode.ts + ┣ 📂loader + ┃ ┗ 📜db.ts ┣ 📂middleware ┃ ┣ 📜authMiddleware.ts ┃ ┗ 📜upload.ts ┣ 📂models ┃ ┣ 📜Book.ts ┃ ┣ 📜Review.ts - ┃ ┣ 📜User.ts - ┃ ┗ 📜index.ts + ┃ ┗ 📜User.ts ┣ 📂others ┃ ┗ 📂slack ┃ ┃ ┣ 📜slack.ts @@ -119,15 +103,9 @@ SOPT 29th APPJAM ┃ ┃ ┣ 📜book.spec.ts ┃ ┃ ┣ 📜review.spec.ts ┃ ┃ ┗ 📜user.spec.ts - ┣ 📜.sequelizerc.ts ┗ 📜index.ts ``` -
- -# 🪜 ERD - -![image](https://user-images.githubusercontent.com/68222629/150541524-3583fe28-fd09-4e4b-813f-37fb0c37e99d.png)
@@ -159,23 +137,28 @@ SOPT 29th APPJAM # 📄 API -| Route | URI | HTTP
메서드 | 설명 | 담당 | 완료 | -| :----: | :------------------------------ | :------------: | :-----------------------: | :--: | :--: | -| Auth | /auth/email/?email= | `GET` | 이메일 유효성 검사 | 동근 | 🧐 | -| | /auth/nickname/?nickname= | `GET` | 닉네임 유효성 검사 | 서현 | 🧐 | -| | /auth/login | `POST` | 유저 로그인 | 서현 | 🧐 | -| | /auth/signup | `POST` | 회원가입 | 동근 | 🧐 | -| | /auth/check | `GET` | 로그인 여부 판별 | 서현 | 🧐 | -| User | /user/myInfo | `GET` | 내 정보 조회 | 서현 | 🧐 | -| | /user/img | `PATCH` | 프로필 사진 수정 | 서현 | 🧐 | -| Book | /book | `POST` | 서재 / 리뷰에 책 추가하기 | 동근 | 🧐 | -| | /book | `GET` | 서재 책 전체 조회 | 동근 | 🧐 | -| Review | /review/before/:reviewId | `PATCH` | 독서 전 단계 | 성용 | 🧐 | -| | /review/:reviewId/question-list | `GET` | 질문 리스트 조회 | 성용 | 🧐 | -| | /review/now/:reviewId | `PATCH` | 독서 중 단계 | 성용 | 🧐 | -| | /review/:reviewId | `GET` | 리뷰 조회 | 서현 | 🧐 | -| | /review/:reviewId | `PATCH` | 리뷰 수정 | 동근 | 🧐 | -| | /review/:reviewId | `DELETE` | 리뷰 삭제 | 성용 | 🧐 | +| Route | URI | HTTP
메서드 | 설명 | +| :----: | :------------------------------ | :------------: | :-----------------------: | +| Auth | /auth/email/?email= | `GET` | 이메일 유효성 검사 | +| | /auth/nickname/?nickname= | `GET` | 닉네임 유효성 검사 | +| | /auth/login | `POST` | 유저 로그인 | +| | /auth/signup | `POST` | 회원가입 | +| | /auth/check | `GET` | 로그인 여부 판별 | +| User | /user/myInfo | `GET` | 내 정보 조회 | +| | /user/img | `PATCH` | 프로필 사진 수정 | +| Book | /book | `POST` | 서재 / 리뷰에 책 추가하기 | +| | /book | `GET` | 서재 책 전체 조회 | +| | /book/pre | `GET` | 서재 독서 전 조회 | +| | /book/peri | `GET` | 서재 독서 중 조회 | +| | /book/post | `GET` | 서재 독서 완료 조회 | +| Review | /review/before/:reviewId | `PATCH` | 독서 전 단계 | +| | /review/:reviewId/question-list | `GET` | 질문 리스트 조회 | +| | /review/now/:reviewId | `PATCH` | 독서 중 단계 | +| | /review/:reviewId | `GET` | 리뷰 조회 | +| | /review/:reviewId | `PATCH` | 리뷰 수정 | +| | /review/:reviewId | `DELETE` | 리뷰 삭제 | +| | /review/:reviewId/pre | `GET` | 독서 전 리뷰 조회 | +| | /review/:reviewId/peri | `GET` | 독서 후 리뷰 조회 |
@@ -189,4 +172,4 @@ SOPT 29th APPJAM | 1.0.1 | bug fix, add api | [📄](https://github.com/TeamBookTez/booktez-server/releases/tag/v1.0.1) | 2022.02.10 | | 1.0.2 | bug fix, modify api | [📄](https://github.com/TeamBookTez/booktez-server/releases/tag/v1.0.2) | 2022.02.21 | | 2.0.0 | switch database
from postgreSQL to mongoDB | [📄](https://github.com/TeamBookTez/booktez-server/releases/tag/v2.0.0) | 2022.03.03 | -| 2.0.1 | bug fix | [📄](https://github.com/TeamBookTez/booktez-server/releases/tag/v2.0.`) | 2022.03.06 | +| 2.0.1 | bug fix | [📄](https://github.com/TeamBookTez/booktez-server/releases/tag/v2.0.1) | 2022.03.06 | diff --git a/package-lock.json b/package-lock.json index 31fbf3b..b9727ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -951,7 +951,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -1214,6 +1213,15 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cron-parser": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", + "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", + "requires": { + "is-nan": "^1.3.2", + "luxon": "^1.26.0" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1292,7 +1300,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -2189,7 +2196,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2372,8 +2378,7 @@ "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "has-tostringtag": { "version": "1.0.0", @@ -2623,6 +2628,15 @@ "is-path-inside": "^3.0.2" } }, + "is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -3062,6 +3076,11 @@ } } }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -3084,6 +3103,11 @@ "es5-ext": "~0.10.2" } }, + "luxon": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", + "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3561,6 +3585,16 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-nl4JTiZ7ZQDc97MmpTq9BQjYhq7gOtoh7SiPH069gBFBj0PzD8HI7zyFs6rzqL8Y5tTiEEYLxgtbx034YPrbyQ==", + "requires": { + "cron-parser": "^3.5.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + } + }, "nodemon": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", @@ -3636,8 +3670,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object.assign": { "version": "4.1.2", @@ -4615,6 +4648,11 @@ "smart-buffer": "^4.2.0" } }, + "sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index f8cfade..c893c66 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mongoose-validator": "^2.1.0", "multer": "^1.4.2", "multer-s3": "^2.9.0", + "node-schedule": "^2.1.0", "pg": "^8.7.1", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.1.13", diff --git a/src/config/index.ts b/src/config/index.ts index de2535e..bf66c5c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,31 +1,41 @@ import dotenv from "dotenv"; -// Set the NODE_ENV to 'development' by default +// 개발 환경 설정 +// default: 'development' process.env.NODE_ENV = process.env.NODE_ENV || "development"; const envFound = dotenv.config(); if (envFound.error) { - // This error should crash whole process - + // 모든 프로세스 중지 throw new Error("⚠️ Couldn't find .env file ⚠️"); } export default { - /** - * Your favorite port - */ + // 포트 번호 port: parseInt(process.env.PORT, 10), - /** - * Your secret sauce - */ + // mongoDB 주소 mongoURI: process.env.MONGODB_URI, - jwtSecret: process.env.JWT_SECRET, - jwtAlgorithm: process.env.JWT_ALGO, + // slack WebHook 주소 + slackURI: process.env.DEV_WEB_HOOK_ERROR_MONITORING, + + // 기본 이미지 + defaultImg: { + user: process.env.DEFAULT_IMG, + book: process.env.DEFAULT_BOOK_IMG, + }, + + // jwt 관련 + jwt: { + secret: process.env.JWT_SECRET, + algorithm: process.env.JWT_ALGO, + }, - // S3 버킷 연결 부분 - awsBucket: process.env.AWS_BUCKET, - awsS3AccessKey: process.env.AWS_ACCESS_KEY, - awsS3SecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + // S3 관련 + aws: { + bucket: process.env.AWS_BUCKET, + s3AccessKey: process.env.AWS_ACCESS_KEY, + s3SecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, }; diff --git a/src/controller/auth.ts b/src/controller/auth.ts index affdc39..a7e89e9 100644 --- a/src/controller/auth.ts +++ b/src/controller/auth.ts @@ -334,12 +334,50 @@ const getLoginFlagController = async (req: Request, res: Response) => { } }; +/** + * @회원탈퇴 + * @route Patch /auth/withdraw + * @access private + * @err + */ +const patchWithdrawController = async (req: Request, res: Response) => { + try { + const resData = await authService.patchWithdrawService(req.user.id); + + if (resData === constant.NON_EXISTENT_USER) { + return response.basicResponse( + res, + returnCode.BAD_REQUEST, + false, + "이미 삭제된 유저입니다." + ); + } + + return response.basicResponse( + res, + returnCode.OK, + true, + "삭제가 완료되었습니다." + ); + } catch (err) { + slack.slackWebhook(req, err.message); + console.error(err.message); + return response.basicResponse( + res, + returnCode.INTERNAL_SERVER_ERROR, + false, + "서버 오류" + ); + } +}; + const authController = { getEmailController, getNicknameController, postLoginController, postSignupController, getLoginFlagController, + patchWithdrawController, }; export default authController; diff --git a/src/controller/book.ts b/src/controller/book.ts index bd14ae9..41b13d9 100644 --- a/src/controller/book.ts +++ b/src/controller/book.ts @@ -189,12 +189,65 @@ const getBookPostController = async (req: Request, res: Response) => { } }; +/** + * @서재 중복검사 + * @route GET /book/exist/:isbn + * @access private + */ +const getBookExistController = async (req: Request, res: Response) => { + try { + const resData = await bookService.getBookExistService( + req.user.id, + req.params.isbn + ); + + if (resData === constant.NULL_VALUE) { + return response.basicResponse( + res, + returnCode.BAD_REQUEST, + false, + "필요한 값이 없습니다." + ); + } + + if (resData === constant.VALUE_ALREADY_EXIST) { + return response.dataResponse( + res, + returnCode.OK, + "이미 추가된 책입니다.", + true, + { isExist: true } + ); + } + + if (resData === constant.SUCCESS) { + return response.dataResponse( + res, + returnCode.OK, + "추가할 수 있는 책입니다.", + true, + { isExist: false } + ); + } + } catch (err) { + slack.slackWebhook(req, err.message); + console.error(err.message); + return response.basicResponse( + res, + returnCode.INTERNAL_SERVER_ERROR, + false, + "서버 오류" + ); + } +}; + const bookController = { postBookController, getBookController, getBookPreController, getBookPeriController, getBookPostController, + getBookExistController, }; export default bookController; diff --git a/src/index.ts b/src/index.ts index 5d53aa4..10f3c04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,10 @@ import express from "express"; import cors from "cors"; import router from "./router"; import connectDB from "./loader/db"; +import config from "./config"; + +// scheduler +import { userScan } from "./scheduler/userScheduler"; const app = express(); @@ -12,7 +16,7 @@ app.use(express.urlencoded()); app.use(express.json()); // Port Host -const PORT: number = parseInt(process.env.PORT as string, 10) || 3000 || 8080; +const PORT: number = config.port || 3000 || 8080; // allow cors app.use( @@ -39,6 +43,9 @@ app.use(function (err, req, res, next) { // render the error page res.status(err.status || 500); res.json({ error: err }); + + // scheduler + userScan; }); const server = app diff --git a/src/interface/IUser.ts b/src/interface/IUser.ts index 9b07e09..9881777 100644 --- a/src/interface/IUser.ts +++ b/src/interface/IUser.ts @@ -8,6 +8,7 @@ export interface IUser { img: string; refresh_token: string; email_code: string; + expired_at: Date; created_at: Date; updated_at: Date; is_deleted: boolean; diff --git a/src/loader/db.ts b/src/loader/db.ts index 89b1d53..c76ab2f 100644 --- a/src/loader/db.ts +++ b/src/loader/db.ts @@ -20,7 +20,12 @@ const connectDB = async () => { console.log("Review Collection is created!"); }); - console.log("Mongoose Connected ..."); + const uri = config.mongoURI; + console.log( + "\nMongoose Connected... [" + + uri.substring(uri.lastIndexOf("/") + 1, uri.length) + + "]\n" + ); } catch (err) { console.error(err.message); process.exit(1); diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index bbc69b9..8d1c927 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -28,7 +28,7 @@ export const auth = async (req: Request, res: Response, next) => { // Verify token try { const token: string = req.headers.authorization; - const decoded = jwt.verify(token, config.jwtSecret); + const decoded = jwt.verify(token, config.jwt.secret); const user = await User.findById(decoded.user.id).where( keysToSnake({ isDeleted: false }) @@ -79,7 +79,7 @@ export const isLogin = async (req: Request, res: Response, next) => { // 적합한 토큰이 있을 경우 // 로그인 상태 const token: string = req.headers.authorization; - const decoded = jwt.verify(token, config.jwtSecret); + const decoded = jwt.verify(token, config.jwt.secret); const user = await User.findById(decoded.user.id).where( keysToSnake({ isDeleted: false }) diff --git a/src/middleware/upload.ts b/src/middleware/upload.ts index 916ff9a..2438fe0 100644 --- a/src/middleware/upload.ts +++ b/src/middleware/upload.ts @@ -1,17 +1,17 @@ import aws from "aws-sdk"; import multer from "multer"; import multerS3 from "multer-s3"; -import config from "../config/index"; +import config from "../config"; const s3 = new aws.S3({ - accessKeyId: config.awsS3AccessKey, - secretAccessKey: config.awsS3SecretAccessKey, + accessKeyId: config.aws.s3AccessKey, + secretAccessKey: config.aws.s3SecretAccessKey, }); const upload = multer({ storage: multerS3({ s3: s3, - bucket: config.awsBucket + "/user_profile", + bucket: config.aws.bucket + "/user_profile", contentType: multerS3.AUTO_CONTENT_TYPE, acl: "public-read", key: (req, file, cb) => { diff --git a/src/models/Book.ts b/src/models/Book.ts index db5dc3c..1fc7753 100644 --- a/src/models/Book.ts +++ b/src/models/Book.ts @@ -1,6 +1,5 @@ import mongoose from "mongoose"; -import dotenv from "dotenv"; -dotenv.config(); +import config from "../config"; // interface import { IBook } from "../interface/IBook"; @@ -37,7 +36,7 @@ const BookSchema = new mongoose.Schema({ thumbnail: { type: String, required: false, - default: process.env.DEFAULT_BOOK_IMG, + default: config.defaultImg.book, }, // 생성 일자 publication_dt: { diff --git a/src/models/User.ts b/src/models/User.ts index 4287d07..7e43719 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,6 +1,5 @@ import mongoose from "mongoose"; -import dotenv from "dotenv"; -dotenv.config(); +import config from "../config"; // interface import { IUser } from "../interface/IUser"; @@ -23,7 +22,7 @@ const UserSchema = new mongoose.Schema({ img: { type: String, required: false, - default: process.env.DEFAULT_IMG, + default: config.defaultImg.user, }, // 리프레시 토큰 refresh_token: { @@ -35,6 +34,13 @@ const UserSchema = new mongoose.Schema({ type: String, required: false, }, + + // 만료 일자 + expired_at: { + type: Date, + required: false, + }, + // 생성 일자 created_at: { type: Date, diff --git a/src/others/slack/slackAPI.ts b/src/others/slack/slackAPI.ts index df865cd..20c4a04 100644 --- a/src/others/slack/slackAPI.ts +++ b/src/others/slack/slackAPI.ts @@ -1,11 +1,9 @@ import axios from "axios"; -import dotenv from "dotenv"; - -dotenv.config(); +import config from "../../config"; // 슬랙 Webhook에서 발급받은 endpoint를 .env 파일에서 끌어옴 // endpoint 자체는 깃허브에 올라가면 안 되기 때문! -const DEV_WEB_HOOK_ERROR_MONITORING = process.env.DEV_WEB_HOOK_ERROR_MONITORING; +const DEV_WEB_HOOK_ERROR_MONITORING = config.slackURI; const sendMessageToSlack = ( message: string, diff --git a/src/router/auth.ts b/src/router/auth.ts index 844ea66..a1e86d3 100644 --- a/src/router/auth.ts +++ b/src/router/auth.ts @@ -1,7 +1,7 @@ import express from "express"; // Middleware -import { isLogin } from "../middleware/authMiddleware"; +import { auth, isLogin } from "../middleware/authMiddleware"; // Controller import authController from "../controller/auth"; @@ -13,5 +13,6 @@ router.get("/nickname", authController.getNicknameController); router.post("/login", authController.postLoginController); router.post("/signup", authController.postSignupController); router.get("/check", isLogin, authController.getLoginFlagController); +router.patch("/withdraw", auth, authController.patchWithdrawController); module.exports = router; diff --git a/src/router/book.ts b/src/router/book.ts index aecd663..b881bed 100644 --- a/src/router/book.ts +++ b/src/router/book.ts @@ -13,5 +13,6 @@ router.get("/", auth, bookController.getBookController); router.get("/pre", auth, bookController.getBookPreController); router.get("/peri", auth, bookController.getBookPeriController); router.get("/post", auth, bookController.getBookPostController); +router.get("/exist/:isbn", auth, bookController.getBookExistController); module.exports = router; diff --git a/src/scheduler/userScheduler.ts b/src/scheduler/userScheduler.ts new file mode 100644 index 0000000..af781c0 --- /dev/null +++ b/src/scheduler/userScheduler.ts @@ -0,0 +1,28 @@ +import schedule from "node-schedule"; + +// models +import Review from "../models/Review"; +import User from "../models/User"; + +// library +import { keysToSnake, keysToCamel } from "../library/convertSnakeToCamel"; + +export const userScan = schedule.scheduleJob("0 0 0 * * *", async () => { + // 현재 시간 + const current = new Date(new Date(Date.now()).setUTCMinutes(0, 0, 0)); + console.log("Scanning users...[" + current + "]"); + + // 삭제 예정 유저 + const deletedUsers = await User.find(keysToSnake({ isDeleted: true })); + + deletedUsers.forEach(async (user) => { + // 현재 날짜가 만료 날짜 이후 + if (current.getTime() >= user.expired_at.getTime()) { + // 해당 유저가 가진 리뷰 모두 삭제 + await Review.deleteMany(keysToSnake({ userID: user._id })); + // 해당 유저 삭제 + await user.deleteOne(keysToSnake({ _id: user._id })); + } + }); + console.log("Complete scanning..."); +}); diff --git a/src/service/auth.ts b/src/service/auth.ts index cd39920..eccb3d2 100644 --- a/src/service/auth.ts +++ b/src/service/auth.ts @@ -1,4 +1,4 @@ -import index from "../config"; +import config from "../config"; import mongoose from "mongoose"; // library @@ -10,7 +10,7 @@ import { checkNicknameValid, checkPasswordValid, } from "../library/checkValidation"; -import { keysToSnake } from "../library/convertSnakeToCamel"; +import { keysToSnake, keysToCamel } from "../library/convertSnakeToCamel"; // model import User from "../models/User"; @@ -122,7 +122,7 @@ const postLoginService = async (email: string, password: string) => { }; const nickname = user.nickname; const userEmail = user.email; - const token = jwt.sign(payload, index.jwtSecret, { expiresIn: "14d" }); + const token = jwt.sign(payload, config.jwt.secret, { expiresIn: "14d" }); return { email: userEmail, nickname, token }; }; @@ -193,7 +193,7 @@ const postSignupService = async ( }, }; - const token = jwt.sign(payload, index.jwtSecret, { + const token = jwt.sign(payload, config.jwt.secret, { expiresIn: "14d", }); return token; @@ -209,12 +209,41 @@ const getLoginFlagService = async (isLogin: Boolean) => { return { isLogin }; }; +/** + * @회원탈퇴 + * @route Patch /auth/withdraw + * @access private + * @err + */ +const patchWithdrawService = async (userId: string) => { + const user = await User.findById(new mongoose.Types.ObjectId(userId)); + + // snake to camel + const originUser = keysToCamel(user); + const camelUser = keysToCamel(originUser.Doc); + + if (camelUser.isDeleted) { + return constant.NON_EXISTENT_USER; + } + + // 삭제 + await user.updateOne({ $set: keysToSnake({ isDeleted: true }) }); + + // 만료 날짜 + const date = new Date(Date.now() + 30 * 24 * 3600 * 1000); + + // 시간 아래는 0 으로 초기화 + const expiredAt = date.setUTCMinutes(0, 0, 0); + await user.updateOne({ $set: keysToSnake({ expiredAt }) }); +}; + const authService = { getEmailService, getNicknameService, postLoginService, postSignupService, getLoginFlagService, + patchWithdrawService, }; export default authService; diff --git a/src/service/book.ts b/src/service/book.ts index 5795548..2391d36 100644 --- a/src/service/book.ts +++ b/src/service/book.ts @@ -2,7 +2,12 @@ import mongoose from "mongoose"; // library import constant from "../library/constant"; -import { keysToSnake, keysToCamel } from "../library/convertSnakeToCamel"; +import { + keysToSnake, + keysToCamel, + toSnakeString, +} from "../library/convertSnakeToCamel"; +import { isValidObjectId } from "mongoose"; // model import User from "../models/User"; @@ -250,12 +255,62 @@ const getBookPostService = async (userId: string) => { return { books: books }; }; +/** + * @서재 중복검사 + * @route GET /book/exist/:isbn + * @access private + */ +// TODO: 책 검사하는 과정에서 isbn이 2개 들어오는지 클라랑 이야기해보기 +// TODO: isbn 형식값 검사 필요 고민 +const getBookExistService = async (userId: string, isbn: string) => { + // 필요한 값이 없는 경우 + if (!userId || !isbn) { + return constant.NULL_VALUE; + } + + isbn = isbn.trim(); + let isbnOne: string, isbnTwo: string; + + // isbn이 2개일 경우, 1개일 경우 + if (/\s/.test(isbn)) { + [isbnOne, isbnTwo] = isbn.split(" "); + } else { + isbnOne = isbn; + isbnTwo = ""; + } + + const reviews = await Review.find( + keysToSnake({ + userId, + isDeleted: false, + }) + ).populate(toSnakeString("bookId")); + + const existReview = reviews.filter((review) => { + if ( + review.book_id.isbn === isbnOne || + review.book_id.isbn_sub === isbnOne || + review.book_id.isbn === isbnTwo || + review.book_id.isbn_sub === isbnTwo + ) { + return review; + } + }); + + if (existReview.length > 0) { + return constant.VALUE_ALREADY_EXIST; + } + + return constant.SUCCESS; +}; + const bookService = { postBookService, getBookService, getBookPreService, getBookPeriService, getBookPostService, + getBookExistService, }; export default bookService;