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
   
-```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
-
-
@@ -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;