From c15495f2e82422d9cc5619d054136ae82bff29a2 Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 6 Oct 2023 18:36:33 +0900 Subject: [PATCH 001/151] Fix: skip validation for trust proxy setting in rate-limit middleware --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/middlewares/limitRate.js | 4 ++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cdb57f4f..20ec12eb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "eslint-config-prettier": "^8.3.0", "express": "^4.17.1", "express-formidable": "^1.2.0", - "express-rate-limit": "^6.6.0", + "express-rate-limit": "^7.1.0", "express-session": "^1.17.3", "express-validator": "^6.14.0", "firebase-admin": "^11.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2249f8ac..8e1b598e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,8 +60,8 @@ dependencies: specifier: ^1.2.0 version: 1.2.0 express-rate-limit: - specifier: ^6.6.0 - version: 6.8.1(express@4.18.2) + specifier: ^7.1.0 + version: 7.1.0(express@4.18.2) express-session: specifier: ^1.17.3 version: 1.17.3 @@ -5088,9 +5088,9 @@ packages: formidable: 1.2.6 dev: false - /express-rate-limit@6.8.1(express@4.18.2): - resolution: {integrity: sha512-xJyudsE60CsDShK74Ni1MxsldYaIoivmG3ieK2tAckMsYCBewEuGalss6p/jHmFFnqM9xd5ojE0W2VlanxcOKg==} - engines: {node: '>= 14.0.0'} + /express-rate-limit@7.1.0(express@4.18.2): + resolution: {integrity: sha512-pwKOMedrpJJeINON/9jhAa18udV2qwxPZSoklPZK8pmXxUyE5uXaptiwjGw8bZILbxqfUZ/p8pQA99ODjSgA5Q==} + engines: {node: '>= 16'} peerDependencies: express: ^4 || ^5 dependencies: diff --git a/src/middlewares/limitRate.js b/src/middlewares/limitRate.js index 4cba6af3..c5069c8f 100644 --- a/src/middlewares/limitRate.js +++ b/src/middlewares/limitRate.js @@ -5,6 +5,10 @@ const limiter = rateLimit({ max: 1500, // Limit each IP to 1500 requests per `window` (here, per 15 minutes) standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers + validate: { + default: true, + trustProxy: false, // Disable the validation error caused by 'trust proxy' set to true + }, }); module.exports = limiter; From 07f181a0632467efbeacc68e490d79b38c3495eb Mon Sep 17 00:00:00 2001 From: withsang Date: Tue, 7 Nov 2023 20:35:01 +0900 Subject: [PATCH 002/151] Merge branch 'origin/main' From 387aee7d963eba6b2674de381bdc3cbf6672bb6f Mon Sep 17 00:00:00 2001 From: withsang Date: Tue, 7 Nov 2023 20:38:15 +0900 Subject: [PATCH 003/151] Fix: validate proxy in rateLimit middleware --- src/middlewares/limitRate.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/middlewares/limitRate.js b/src/middlewares/limitRate.js index c5069c8f..4cba6af3 100644 --- a/src/middlewares/limitRate.js +++ b/src/middlewares/limitRate.js @@ -5,10 +5,6 @@ const limiter = rateLimit({ max: 1500, // Limit each IP to 1500 requests per `window` (here, per 15 minutes) standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers - validate: { - default: true, - trustProxy: false, // Disable the validation error caused by 'trust proxy' set to true - }, }); module.exports = limiter; From b05405217b8dc7061af5a29898c5f3facc8901d7 Mon Sep 17 00:00:00 2001 From: withsang Date: Thu, 16 Nov 2023 02:55:49 +0900 Subject: [PATCH 004/151] fix(github): disable unit test --- .github/workflows/test_ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index 29f7f21b..b08563d1 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -15,6 +15,8 @@ jobs: node-version: ['18.x'] mongodb-version: ['5.0'] steps: + - name: Exit because unit tests are not implemented in TypeScript for now + run: exit 1 - name: Start MongoDB run: sudo docker run --name mongodb -d -p 27017:27017 mongo:${{ matrix.mongodb-version }} - uses: actions/checkout@v3 From cf5da433b1b6fc9becd4acdf0a5ff5db4ad034d9 Mon Sep 17 00:00:00 2001 From: withsang Date: Thu, 16 Nov 2023 03:18:54 +0900 Subject: [PATCH 005/151] feat(typescript): init typescript project --- .dockerignore | 3 + .eslintignore | 7 + .eslintrc.js => .eslintrc.cjs | 0 .gitignore | 2 + .prettierignore | 9 +- loadenv.js | 47 ------ nodemon.json | 2 + package.json | 16 ++- pnpm-lock.yaml | 264 ++++++++++++++++++++++++++++------ app.js => src/index.ts | 14 +- src/loadenv.ts | 59 ++++++++ tsconfig.json | 20 +++ 12 files changed, 342 insertions(+), 101 deletions(-) create mode 100644 .eslintignore rename .eslintrc.js => .eslintrc.cjs (100%) delete mode 100644 loadenv.js rename app.js => src/index.ts (87%) create mode 100644 src/loadenv.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore index 8ffa241b..bafcba69 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ /node_modules +/dist +.env .env.test .env.production .env.development @@ -6,6 +8,7 @@ *.code-workspace *.swp /logs/*.log +.vscode # AdminJS 관련 디렉토리 .adminjs diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..15e64178 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +package.json +tsconfig.json +.prettierrc.json +.eslintrc.cjs +nodemon.json \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/.gitignore b/.gitignore index a7e86767..bafcba69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /node_modules +/dist +.env .env.test .env.production .env.development diff --git a/.prettierignore b/.prettierignore index dd87e2d7..15e64178 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,7 @@ -node_modules -build +node_modules/ +dist/ +package.json +tsconfig.json +.prettierrc.json +.eslintrc.cjs +nodemon.json \ No newline at end of file diff --git a/loadenv.js b/loadenv.js deleted file mode 100644 index f7224601..00000000 --- a/loadenv.js +++ /dev/null @@ -1,47 +0,0 @@ -// 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옴 -require("dotenv").config({ path: `./.env.${process.env.NODE_ENV}` }); - -module.exports = { - nodeEnv: process.env.NODE_ENV, // required - mongo: process.env.DB_PATH, // required - session: { - secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", // optional - expiry: 14 * 24 * 3600 * 1000, // 14일, ms 단위입니다. - }, - redis: process.env.REDIS_PATH, // optional - sparcssso: { - id: process.env.SPARCSSSO_CLIENT_ID || "", // optional - key: process.env.SPARCSSSO_CLIENT_KEY || "", // optional - }, - port: process.env.PORT || 80, // optional (default = 80) - corsWhiteList: (process.env.CORS_WHITELIST && - JSON.parse(process.env.CORS_WHITELIST)) || [true], // optional (default = [true]) - aws: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID, // required - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // required - s3BucketName: process.env.AWS_S3_BUCKET_NAME, // required - s3Url: - process.env.AWS_S3_URL || - `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional - }, - jwt: { - secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", - option: { - algorithm: "HS256", - // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. - // See https://github.com/sparcs-kaist/taxi-back/issues/415 - issuer: process.env.FRONT_URL || "http://localhost:3000", // optional (default = "http://localhost:3000") - }, - TOKEN_EXPIRED: -3, - TOKEN_INVALID: -2, - }, - googleApplicationCredentials: - process.env.GOOGLE_APPLICATION_CREDENTIALS && - JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS), // optional - testAccounts: - (process.env.TEST_ACCOUNTS && JSON.parse(process.env.TEST_ACCOUNTS)) || [], // optional - slackWebhookUrl: { - report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional - }, - eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), -}; diff --git a/nodemon.json b/nodemon.json index 62f8de10..5d6b9eaa 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,7 @@ { "ignore": ["node_modules/*"], + "watch": ["./src", ".env.development"], + "exec": "tsc && tsc-alias && node dist/index.js", "env": { "TZ": "Asia/Seoul", "NODE_ENV": "development" diff --git a/package.json b/package.json index fecc4769..30d8ba6b 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,12 @@ "main": "app.js", "scripts": { "preinstall": "npx only-allow pnpm", - "start": "cross-env TZ='Asia/Seoul' npx nodemon app.js", - "test": "npm run sample && cross-env TZ='Asia/Seoul' npm run mocha", + "start": "npx tsc && tsc-alias && npx nodemon", "mocha": "cross-env TZ='Asia/Seoul' NODE_ENV=test mocha --recursive --reporter spec --exit", - "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node app.js", + "test": "npm run sample && cross-env TZ='Asia/Seoul' npm run mocha", + "build": "tsc && tsc-alias", + "clean": "rimraf dist/", + "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node dist/index.js", "lint": "npx eslint --fix .", "sample": "cd sampleGenerator && npm start && cd .." }, @@ -55,11 +57,17 @@ "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { + "@types/cors": "^2.8.16", + "@types/express": "^4.17.21", + "@types/node": "^20.9.0", "chai": "^4.3.10", "eslint": "^8.22.0", "eslint-plugin-mocha": "^10.1.0", "mocha": "^10.2.0", "nodemon": "^3.0.1", - "supertest": "^6.2.4" + "rimraf": "^5.0.5", + "supertest": "^6.2.4", + "tsc-alias": "^1.8.8", + "typescript": "^5.2.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d197fbe1..8900aeb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,15 @@ dependencies: version: 4.7.1(winston@3.10.0) devDependencies: + '@types/cors': + specifier: ^2.8.16 + version: 2.8.16 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/node': + specifier: ^20.9.0 + version: 20.9.0 chai: specifier: ^4.3.10 version: 4.3.10 @@ -124,9 +133,18 @@ devDependencies: nodemon: specifier: ^3.0.1 version: 3.0.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.5 supertest: specifier: ^6.2.4 version: 6.3.3 + tsc-alias: + specifier: ^1.8.8 + version: 1.8.8 + typescript: + specifier: ^5.2.2 + version: 5.2.2 packages: @@ -2273,7 +2291,7 @@ packages: resolution: {integrity: sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==} dependencies: '@firebase/util': 1.9.3 - tslib: 2.6.1 + tslib: 2.6.2 dev: false /@firebase/database-compat@0.3.4: @@ -2302,19 +2320,19 @@ packages: '@firebase/logger': 0.4.0 '@firebase/util': 1.9.3 faye-websocket: 0.11.4 - tslib: 2.6.1 + tslib: 2.6.2 dev: false /@firebase/logger@0.4.0: resolution: {integrity: sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==} dependencies: - tslib: 2.6.1 + tslib: 2.6.2 dev: false /@firebase/util@1.9.3: resolution: {integrity: sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==} dependencies: - tslib: 2.6.1 + tslib: 2.6.2 dev: false /@floating-ui/core@1.4.1: @@ -2408,7 +2426,7 @@ packages: requiresBuild: true dependencies: '@grpc/proto-loader': 0.7.8 - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false optional: true @@ -2475,6 +2493,18 @@ packages: warning: 4.0.3 dev: false + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -2551,6 +2581,13 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + /@popperjs/core@2.11.8: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false @@ -3702,24 +3739,21 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: false - /@types/cors@2.8.13: - resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} + /@types/cors@2.8.16: + resolution: {integrity: sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==} dependencies: - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/estree@0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} @@ -3728,27 +3762,25 @@ packages: /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 - dev: false - /@types/express@4.17.17: - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.2 '@types/express-serve-static-core': 4.17.35 '@types/qs': 6.9.7 '@types/serve-static': 1.15.2 - dev: false /@types/glob@8.1.0: resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} requiresBuild: true dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false optional: true @@ -3761,12 +3793,11 @@ packages: /@types/http-errors@2.0.1: resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} - dev: false /@types/jsonwebtoken@9.0.2: resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false /@types/linkify-it@3.0.2: @@ -3798,7 +3829,6 @@ packages: /@types/mime@1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} - dev: false /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -3806,9 +3836,10 @@ packages: dev: false optional: true - /@types/node@20.4.7: - resolution: {integrity: sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==} - dev: false + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} + dependencies: + undici-types: 5.26.5 /@types/object.omit@3.0.0: resolution: {integrity: sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==} @@ -3828,11 +3859,9 @@ packages: /@types/qs@6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: false /@types/range-parser@1.2.4: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: false /@types/react-transition-group@4.4.6: resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} @@ -3851,7 +3880,7 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false /@types/rimraf@3.0.2: @@ -3859,7 +3888,7 @@ packages: requiresBuild: true dependencies: '@types/glob': 8.1.0 - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false optional: true @@ -3871,16 +3900,14 @@ packages: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/serve-static@1.15.2: resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} dependencies: '@types/http-errors': 2.0.1 '@types/mime': 1.3.2 - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/throttle-debounce@2.1.0: resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==} @@ -3901,7 +3928,7 @@ packages: /@types/whatwg-url@8.2.2: resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 '@types/webidl-conversions': 7.0.0 dev: false @@ -4046,6 +4073,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -4059,6 +4091,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -4548,6 +4585,11 @@ packages: engines: {node: '>= 6'} dev: false + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: true + /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: false @@ -4881,6 +4923,10 @@ packages: dev: false optional: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -4898,6 +4944,10 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} dev: false @@ -4925,8 +4975,8 @@ packages: engines: {node: '>=10.2.0'} dependencies: '@types/cookie': 0.4.1 - '@types/cors': 2.8.13 - '@types/node': 20.4.7 + '@types/cors': 2.8.16 + '@types/node': 20.9.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -5380,7 +5430,7 @@ packages: '@fastify/busboy': 1.2.1 '@firebase/database-compat': 0.3.4 '@firebase/database-types': 0.10.4 - '@types/node': 20.4.7 + '@types/node': 20.9.0 jsonwebtoken: 9.0.2 jwks-rsa: 3.0.1 node-forge: 1.3.1 @@ -5427,6 +5477,14 @@ packages: is-callable: 1.2.7 dev: false + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -5541,6 +5599,18 @@ packages: dependencies: is-glob: 4.0.3 + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + /glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} dependencies: @@ -5966,11 +6036,20 @@ packages: engines: {node: '>=0.10.0'} dev: false + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + /jest-worker@26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 merge-stream: 2.0.0 supports-color: 7.2.0 dev: false @@ -6107,7 +6186,7 @@ packages: resolution: {integrity: sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==} engines: {node: '>=14'} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.21 '@types/jsonwebtoken': 9.0.2 debug: 4.3.4 jose: 4.14.4 @@ -6323,6 +6402,13 @@ packages: get-func-name: 2.0.2 dev: true + /lru-cache@10.0.2: + resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==} + engines: {node: 14 || >=16.14} + dependencies: + semver: 7.5.4 + dev: true + /lru-cache@4.0.2: resolution: {integrity: sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==} dependencies: @@ -6513,12 +6599,24 @@ packages: dev: false optional: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} requiresBuild: true dev: false optional: true + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -6620,6 +6718,11 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + dev: true + /nanoid@3.3.3: resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6890,6 +6993,14 @@ packages: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: false + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.0.2 + minipass: 7.0.4 + dev: true + /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} dev: false @@ -6931,6 +7042,13 @@ packages: find-up: 3.0.0 dev: false + /plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + dependencies: + queue-lit: 1.5.2 + dev: true + /polished@3.7.2: resolution: {integrity: sha512-pQKtpZGmsZrW8UUpQMAnR7s3ppHeMQVNyMDKtUyKwuvDmklzcEyM5Kllb3JyE/sE/x7arDmyd35i+4vp99H6sQ==} engines: {node: '>=10'} @@ -7150,7 +7268,7 @@ packages: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.4.7 + '@types/node': 20.9.0 long: 5.2.3 dev: false optional: true @@ -7170,7 +7288,7 @@ packages: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.4.7 + '@types/node': 20.9.0 long: 5.2.3 dev: false optional: true @@ -7217,6 +7335,11 @@ packages: deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. dev: false + /queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -7618,6 +7741,14 @@ packages: dependencies: glob: 7.2.3 + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: true + /rollup-plugin-terser@7.0.2(rollup@2.79.1): resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser @@ -7772,6 +7903,11 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: @@ -7899,6 +8035,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -7911,6 +8056,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -8145,6 +8297,18 @@ packages: engines: {node: '>= 14.0.0'} dev: false + /tsc-alias@1.8.8: + resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==} + hasBin: true + dependencies: + chokidar: 3.5.3 + commander: 9.5.0 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + dev: true + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} requiresBuild: true @@ -8196,6 +8360,12 @@ packages: mime-types: 2.1.35 dev: false + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} dev: false @@ -8225,6 +8395,9 @@ packages: dev: false optional: true + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -8491,6 +8664,15 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} diff --git a/app.js b/src/index.ts similarity index 87% rename from app.js rename to src/index.ts index f8c24842..a24277e9 100644 --- a/app.js +++ b/src/index.ts @@ -1,10 +1,10 @@ -// 모듈 require -const express = require("express"); -const http = require("http"); -const { nodeEnv, port: httpPort, eventConfig } = require("./loadenv"); -const logger = require("./src/modules/logger"); -const { connectDatabase } = require("./src/modules/stores/mongo"); -const { startSocketServer } = require("./src/modules/socket"); +// 모듈 import +import express from "express"; +import http from "http"; +import { nodeEnv, port as httpPort, eventConfig } from "@/loadenv"; +import logger from "@/modules/logger"; +import { connectDatabase } from "@/modules/stores/mongo"; +import { startSocketServer } from "@/modules/socket"; // Firebase Admin 초기설정 require("./src/modules/fcm").initializeApp(); diff --git a/src/loadenv.ts b/src/loadenv.ts new file mode 100644 index 00000000..88cca713 --- /dev/null +++ b/src/loadenv.ts @@ -0,0 +1,59 @@ +// 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옵니다. +import dotenv from "dotenv"; + +if (process.env.NODE_ENV === undefined) { + // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. + console.error("NODE_ENV 환경변수가 설정되어 있지 않습니다."); + process.exit(1); +} +dotenv.config({ path: `./.env.${process.env.NODE_ENV}` }); + +if (process.env.DB_PATH === undefined) { + // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. + console.error("DB_PATH 환경변수가 설정되어 있지 않습니다."); + process.exit(1); +} + +export const nodeEnv = process.env.NODE_ENV; // required +export const mongo = process.env.DB_PATH; // required +export const session = { + secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", // optional + expiry: 14 * 24 * 3600 * 1000, // 14일, ms 단위입니다. +}; +export const redis = process.env.REDIS_PATH; // optional +export const sparcssso = { + id: process.env.SPARCSSSO_CLIENT_ID || "", // optional + key: process.env.SPARCSSSO_CLIENT_KEY || "", // optional +}; +export const port = process.env.PORT ? parseInt(process.env.PORT) : 80; // optional (default = 80) +export const corsWhiteList = (process.env.CORS_WHITELIST && + JSON.parse(process.env.CORS_WHITELIST)) || [true]; // optional (default = [true]) +export const aws = { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, // required + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // required + s3BucketName: process.env.AWS_S3_BUCKET_NAME, // required + s3Url: + process.env.AWS_S3_URL || + `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional +}; +export const jwt = { + secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", + option: { + algorithm: "HS256", + // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. + // See https://github.com/sparcs-kaist/taxi-back/issues/415 + issuer: process.env.FRONT_URL || "http://localhost:3000", // optional (default = "http://localhost:3000") + }, + TOKEN_EXPIRED: -3, + TOKEN_INVALID: -2, +}; +export const googleApplicationCredentials = + process.env.GOOGLE_APPLICATION_CREDENTIALS && + JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS); // optional +export const testAccounts = + (process.env.TEST_ACCOUNTS && JSON.parse(process.env.TEST_ACCOUNTS)) || []; // optional +export const slackWebhookUrl = { + report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional +}; +export const eventConfig = + process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..0f059817 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "node16", + "moduleResolution": "node16", + "allowJs": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "./src", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["src"], + "exclude": ["dist", "node_modules"] + } + \ No newline at end of file From 972366568fc59790757f69c47c2cff1c3d78ac97 Mon Sep 17 00:00:00 2001 From: withsang Date: Thu, 16 Nov 2023 03:43:45 +0900 Subject: [PATCH 006/151] feat(typescript): type index.ts --- package.json | 2 + pnpm-lock.yaml | 16 +++++++ src/index.ts | 72 ++++++++++++++++++---------- src/loadenv.ts | 4 +- src/middlewares/index.ts | 12 +++++ src/routes/index.ts | 10 ++++ src/schedules/{index.js => index.ts} | 7 +-- tsconfig.json | 2 +- 8 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 src/middlewares/index.ts create mode 100644 src/routes/index.ts rename src/schedules/{index.js => index.ts} (50%) diff --git a/package.json b/package.json index 30d8ba6b..fd1270b2 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,11 @@ "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { + "@types/cookie-parser": "^1.4.6", "@types/cors": "^2.8.16", "@types/express": "^4.17.21", "@types/node": "^20.9.0", + "@types/node-cron": "^3.0.11", "chai": "^4.3.10", "eslint": "^8.22.0", "eslint-plugin-mocha": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8900aeb3..be691a0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,9 @@ dependencies: version: 4.7.1(winston@3.10.0) devDependencies: + '@types/cookie-parser': + specifier: ^1.4.6 + version: 1.4.6 '@types/cors': specifier: ^2.8.16 version: 2.8.16 @@ -118,6 +121,9 @@ devDependencies: '@types/node': specifier: ^20.9.0 version: 20.9.0 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 chai: specifier: ^4.3.10 version: 4.3.10 @@ -3746,6 +3752,12 @@ packages: dependencies: '@types/node': 20.9.0 + /@types/cookie-parser@1.4.6: + resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==} + dependencies: + '@types/express': 4.17.21 + dev: true + /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: false @@ -3836,6 +3848,10 @@ packages: dev: false optional: true + /@types/node-cron@3.0.11: + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + dev: true + /@types/node@20.9.0: resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} dependencies: diff --git a/src/index.ts b/src/index.ts index a24277e9..67648cde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,34 @@ // 모듈 import import express from "express"; +import cookieParser from "cookie-parser"; import http from "http"; -import { nodeEnv, port as httpPort, eventConfig } from "@/loadenv"; + +import { nodeEnv, port as httpPort } from "@/loadenv"; +import { + corsMiddleware, + sessionMiddleware, + informationMiddleware, + responseTimeMiddleware, + limitRateMiddleware, + originValidatorMiddleware, + errorHandler, +} from "@/middlewares"; +import { + authRouter, + logininfoRouter, + userRouter, + roomRouter, + chatRouter, + locationRouter, + reportRouter, + notificationRouter, + adminRouter, + docsRouter, +} from "@/routes"; import logger from "@/modules/logger"; import { connectDatabase } from "@/modules/stores/mongo"; import { startSocketServer } from "@/modules/socket"; +import registerSchedules from "@/schedules"; // Firebase Admin 초기설정 require("./src/modules/fcm").initializeApp(); @@ -23,50 +47,46 @@ app.use(express.json()); if (nodeEnv === "production") app.set("trust proxy", 2); // [Middleware] CORS 설정 -app.use(require("./src/middlewares/cors")); +app.use(corsMiddleware); // [Middleware] 세션 및 쿠키 -const session = require("./src/middlewares/session"); -app.use(session); -app.use(require("cookie-parser")()); +app.use(sessionMiddleware); +app.use(cookieParser()); // [Middleware] Timestamp 및 clientIP 확인 -app.use(require("./src/middlewares/information")); +app.use(informationMiddleware); // [Middleware] API 접근 기록 및 응답 시간을 http response의 헤더에 기록합니다. -app.use(require("./src/middlewares/responseTime")); +app.use(responseTimeMiddleware); // [Router] admin 페이지는 rate limiting을 적용하지 않습니다. -app.use("/admin", require("./src/routes/admin")); +app.use("/admin", adminRouter); // [Middleware] 모든 요청에 대하여 rate limiting 적용 -app.use(require("./src/middlewares/limitRate")); +app.use(limitRateMiddleware); // [Router] Swagger (API 문서) -app.use("/docs", require("./src/routes/docs")); +app.use("/docs", docsRouter); // 2023 추석 이벤트 전용 라우터입니다. -eventConfig && - app.use( - `/events/${eventConfig.mode}`, - require("./src/lottery").lotteryRouter - ); +// eventConfig && +// app.use(`/events/${eventConfig.mode}`, require("@/lottery").lotteryRouter); // [Middleware] 모든 API 요청에 대하여 origin 검증 -app.use(require("./src/middlewares/originValidator")); +app.use(originValidatorMiddleware); // [Router] APIs -app.use("/auth", require("./src/routes/auth")); -app.use("/logininfo", require("./src/routes/logininfo")); -app.use("/users", require("./src/routes/users")); -app.use("/rooms", require("./src/routes/rooms")); -app.use("/chats", require("./src/routes/chats")); -app.use("/locations", require("./src/routes/locations")); -app.use("/reports", require("./src/routes/reports")); -app.use("/notifications", require("./src/routes/notifications")); +app.use("/auth", authRouter); +app.use("/logininfo", logininfoRouter); +app.use("/users", userRouter); +app.use("/rooms", roomRouter); +app.use("/chats", chatRouter); +app.use("/locations", locationRouter); +app.use("/reports", reportRouter); +app.use("/notifications", notificationRouter); // [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. -app.use(require("./src/middlewares/errorHandler")); +app.use(errorHandler); // express 서버 시작 const serverHttp = http @@ -79,4 +99,4 @@ const serverHttp = http app.set("io", startSocketServer(serverHttp)); // [Schedule] 스케줄러 시작 -require("./src/schedules")(app); +registerSchedules(app); diff --git a/src/loadenv.ts b/src/loadenv.ts index 88cca713..6307bdca 100644 --- a/src/loadenv.ts +++ b/src/loadenv.ts @@ -55,5 +55,5 @@ export const testAccounts = export const slackWebhookUrl = { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }; -export const eventConfig = - process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG); +// export const eventConfig = +// process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG); diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 00000000..d25fec2b --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1,12 @@ +// middleware를 모아 export합니다. +export * from "./ajv"; +export { default as authMiddleware } from "./auth"; +export { default as authAdminMiddleware } from "./authAdmin"; +export { default as corsMiddleware } from "./cors"; +export { default as errorHandler } from "./errorHandler"; +export { default as informationMiddleware } from "./information"; +export { default as limitRateMiddleware } from "./limitRate"; +export { default as originValidatorMiddleware } from "./originValidator"; +export { default as responseTimeMiddleware } from "./responseTime"; +export { default as sessionMiddleware } from "./session"; +export { default as validatorMiddleware } from "./validator"; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 00000000..d2084458 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,10 @@ +export { default as adminRouter } from "./admin"; +export { default as authRouter } from "./auth"; +export { default as chatRouter } from "./chats"; +export { default as docsRouter } from "./docs"; +export { default as locationRouter } from "./locations"; +export { default as logininfoRouter } from "./logininfo"; +export { default as notificationRouter } from "./notifications"; +export { default as reportRouter } from "./reports"; +export { default as roomRouter } from "./rooms"; +export { default as userRouter } from "./users"; diff --git a/src/schedules/index.js b/src/schedules/index.ts similarity index 50% rename from src/schedules/index.js rename to src/schedules/index.ts index 97818b92..f57ca9dc 100644 --- a/src/schedules/index.js +++ b/src/schedules/index.ts @@ -1,8 +1,9 @@ -const cron = require("node-cron"); +import { Express } from "express"; +import cron from "node-cron"; -const registerSchedules = (app) => { +const registerSchedules = (app: Express) => { cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); }; -module.exports = registerSchedules; +export default registerSchedules; diff --git a/tsconfig.json b/tsconfig.json index 0f059817..415e2140 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,6 @@ } }, "include": ["src"], - "exclude": ["dist", "node_modules"] + "exclude": ["dist", "node_modules", "src/lottery"] } \ No newline at end of file From 5dfb074b0b24fea695d5e36835680fdd74555d69 Mon Sep 17 00:00:00 2001 From: withsang Date: Tue, 28 Nov 2023 21:01:04 +0900 Subject: [PATCH 007/151] Refactor: add path alias --- src/index.ts | 3 ++- src/middlewares/auth.js | 2 +- src/middlewares/authAdmin.js | 4 ++-- src/middlewares/cors.js | 2 +- src/middlewares/errorHandler.js | 2 +- src/middlewares/responseTime.js | 2 +- src/middlewares/session.js | 2 +- src/modules/adminResource.js | 2 +- src/modules/auths/jwt.js | 2 +- src/modules/auths/login.js | 4 ++-- src/modules/fcm.js | 6 +++--- src/modules/logger.js | 2 +- src/modules/slackNotification.js | 4 ++-- src/modules/socket.js | 4 ++-- src/modules/stores/aws.js | 8 +++++--- src/modules/stores/mongo.js | 4 ++-- src/modules/stores/sessionStore.js | 4 ++-- src/routes/admin.js | 10 +++++----- src/routes/auth.js | 10 +++++----- src/routes/chats.js | 8 ++++---- src/routes/locations.js | 2 +- src/routes/logininfo.js | 2 +- src/routes/notifications.js | 8 ++++---- src/routes/reports.js | 6 +++--- src/routes/rooms.js | 6 +++--- src/routes/users.js | 10 +++++----- src/schedules/notifyAfterArrival.js | 8 ++++---- src/schedules/notifyBeforeDepart.js | 6 +++--- src/services/auth.js | 24 ++++++++++-------------- src/services/auth.mobile.js | 15 ++++++--------- src/services/auth.replace.js | 16 ++++++++-------- src/services/chats.js | 12 ++++++------ src/services/locations.js | 4 ++-- src/services/logininfo.js | 6 +++--- src/services/notifications.js | 8 ++++---- src/services/reports.js | 16 ++++++---------- src/services/rooms.js | 8 ++++---- src/services/users.js | 8 ++++---- src/views/emailPage.js | 2 +- 39 files changed, 122 insertions(+), 130 deletions(-) diff --git a/src/index.ts b/src/index.ts index 67648cde..ef692ea5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,13 +25,14 @@ import { adminRouter, docsRouter, } from "@/routes"; +import { initializeApp } from "@/modules/fcm"; import logger from "@/modules/logger"; import { connectDatabase } from "@/modules/stores/mongo"; import { startSocketServer } from "@/modules/socket"; import registerSchedules from "@/schedules"; // Firebase Admin 초기설정 -require("./src/modules/fcm").initializeApp(); +initializeApp(); // 익스프레스 서버 생성 const app = express(); diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js index e521f9f4..15d1f5a5 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.js @@ -1,6 +1,6 @@ // 로그인된 상태에만 접근할 수 있는 라우터(rooms)를 위한 미들웨어입니다. -const { isLogin, getLoginInfo } = require("../modules/auths/login"); +const { isLogin, getLoginInfo } = require("@/modules/auths/login"); const authMiddleware = (req, res, next) => { if (!isLogin(req)) { diff --git a/src/middlewares/authAdmin.js b/src/middlewares/authAdmin.js index 8db1f2a0..34fc5df6 100644 --- a/src/middlewares/authAdmin.js +++ b/src/middlewares/authAdmin.js @@ -1,7 +1,7 @@ // 관리자 유무를 확인하기 위한 미들웨어입니다. -const { isLogin, getLoginInfo } = require("../modules/auths/login"); -const { userModel, adminIPWhitelistModel } = require("../modules/stores/mongo"); +const { isLogin, getLoginInfo } = require("@/modules/auths/login"); +const { userModel, adminIPWhitelistModel } = require("@/modules/stores/mongo"); const authAdminMiddleware = async (req, res, next) => { try { diff --git a/src/middlewares/cors.js b/src/middlewares/cors.js index 0b644243..a1012607 100644 --- a/src/middlewares/cors.js +++ b/src/middlewares/cors.js @@ -1,5 +1,5 @@ var cors = require("cors"); -const { corsWhiteList } = require("../../loadenv"); +const { corsWhiteList } = require("@/loadenv"); module.exports = cors({ origin: corsWhiteList, diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index 369391c6..d9bdcc0c 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -1,4 +1,4 @@ -const logger = require("../modules/logger"); +const logger = require("@/modules/logger"); /** * Express app에서 사용할 custom global error handler를 정의합니다. diff --git a/src/middlewares/responseTime.js b/src/middlewares/responseTime.js index 1d674364..c042ffb3 100644 --- a/src/middlewares/responseTime.js +++ b/src/middlewares/responseTime.js @@ -1,4 +1,4 @@ -const logger = require("../modules/logger"); +const logger = require("@/modules/logger"); const responseTime = require("response-time"); module.exports = responseTime((req, res, time) => { diff --git a/src/middlewares/session.js b/src/middlewares/session.js index 5412ba1c..b4be3266 100644 --- a/src/middlewares/session.js +++ b/src/middlewares/session.js @@ -1,6 +1,6 @@ const expressSession = require("express-session"); const { nodeEnv, session: sessionConfig } = require("../../loadenv"); -const sessionStore = require("../modules/stores/sessionStore"); +const sessionStore = require("@/modules/stores/sessionStore"); module.exports = expressSession({ secret: sessionConfig.secret, diff --git a/src/modules/adminResource.js b/src/modules/adminResource.js index f5ba3eb8..19deb694 100644 --- a/src/modules/adminResource.js +++ b/src/modules/adminResource.js @@ -1,5 +1,5 @@ const { buildFeature } = require("adminjs"); -const { adminLogModel } = require("./stores/mongo"); +const { adminLogModel } = require("@/modules/stores/mongo"); const createLog = async (req, action, target) => { const newLog = new adminLogModel({ diff --git a/src/modules/auths/jwt.js b/src/modules/auths/jwt.js index 52945347..4e8f7259 100644 --- a/src/modules/auths/jwt.js +++ b/src/modules/auths/jwt.js @@ -1,6 +1,6 @@ const jwt = require("jsonwebtoken"); const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = - require("../../../loadenv").jwt; + require("@/loadenv").jwt; const signJwt = async ({ id, type }) => { const payload = { diff --git a/src/modules/auths/login.js b/src/modules/auths/login.js index 9c72434f..7c4f6baf 100644 --- a/src/modules/auths/login.js +++ b/src/modules/auths/login.js @@ -1,5 +1,5 @@ -const { session: sessionConfig } = require("../../../loadenv"); -const logger = require("../logger"); +const { session: sessionConfig } = require("@/loadenv"); +const logger = require("@/modules/logger"); const getLoginInfo = (req) => { if (req.session.loginInfo) { diff --git a/src/modules/fcm.js b/src/modules/fcm.js index cf6a0433..c9ab01d6 100644 --- a/src/modules/fcm.js +++ b/src/modules/fcm.js @@ -4,9 +4,9 @@ const { deviceTokenModel, notificationOptionModel, topicSubscriptionModel, -} = require("./stores/mongo"); -const logger = require("../modules/logger"); -const { googleApplicationCredentials } = require("../../loadenv"); +} = require("@/modules/stores/mongo"); +const logger = require("./logger"); +const { googleApplicationCredentials } = require("@/loadenv"); /** * credential을 등록합니다. diff --git a/src/modules/logger.js b/src/modules/logger.js index e532e7aa..3baa80d5 100644 --- a/src/modules/logger.js +++ b/src/modules/logger.js @@ -2,7 +2,7 @@ const path = require("path"); const { createLogger, format, transports } = require("winston"); const DailyRotateFileTransport = require("winston-daily-rotate-file"); -const { nodeEnv } = require("../../loadenv"); +const { nodeEnv } = require("@/loadenv"); // logger에서 사용할 포맷들을 정의합니다. const baseFormat = format.combine( diff --git a/src/modules/slackNotification.js b/src/modules/slackNotification.js index dc00e4a0..4ac3d378 100644 --- a/src/modules/slackNotification.js +++ b/src/modules/slackNotification.js @@ -1,6 +1,6 @@ -const { slackWebhookUrl: slackUrl } = require("../../loadenv"); +const { slackWebhookUrl: slackUrl } = require("@/loadenv"); const axios = require("axios"); -const logger = require("../modules/logger"); +const logger = require("./logger"); module.exports.notifyToReportChannel = (reportUser, report) => { if (!slackUrl.report) return; diff --git a/src/modules/socket.js b/src/modules/socket.js index bca99161..eabbbe62 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -1,13 +1,13 @@ const { Server } = require("socket.io"); -const sessionMiddleware = require("../middlewares/session"); +const sessionMiddleware = require("@/middlewares/session"); const logger = require("./logger"); const { getLoginInfo } = require("./auths/login"); const { roomModel, userModel, chatModel } = require("./stores/mongo"); const { getS3Url } = require("./stores/aws"); const { getTokensOfUsers, sendMessageByTokens } = require("./fcm"); -const { corsWhiteList } = require("../../loadenv"); +const { corsWhiteList } = require("@/loadenv"); const { chatPopulateOption } = require("./populates/chats"); /** diff --git a/src/modules/stores/aws.js b/src/modules/stores/aws.js index bade704b..2947410c 100644 --- a/src/modules/stores/aws.js +++ b/src/modules/stores/aws.js @@ -1,6 +1,6 @@ -const { aws: awsEnv } = require("../../../loadenv"); +const { aws: awsEnv } = require("@/loadenv"); -const logger = require("../logger"); +const logger = require("@/modules/logger"); // Load the AWS-SDK and s3 const AWS = require("aws-sdk"); AWS.config.update({ @@ -103,7 +103,9 @@ module.exports.sendReportEmail = (reportedEmail, report, html) => { }, Subject: { Charset: "UTF-8", - Data: `[SPARCS TAXI] 신고가 접수되었습니다 (사유: ${reportTypeMap[report.type]})`, + Data: `[SPARCS TAXI] 신고가 접수되었습니다 (사유: ${ + reportTypeMap[report.type] + })`, }, }, Source: "taxi.sparcs@gmail.com", diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index f05f266a..f4741652 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -1,8 +1,8 @@ const mongoose = require("mongoose"); const Schema = mongoose.Schema; -const { mongo: mongoUrl } = require("../../../loadenv"); -const logger = require("../logger"); +const { mongo: mongoUrl } = require("@/loadenv"); +const logger = require("@/modules/logger"); const userSchema = Schema({ name: { type: String, required: true }, //실명 diff --git a/src/modules/stores/sessionStore.js b/src/modules/stores/sessionStore.js index fca4da55..3d247d54 100644 --- a/src/modules/stores/sessionStore.js +++ b/src/modules/stores/sessionStore.js @@ -6,8 +6,8 @@ const { redis: redisUrl, mongo: mongoUrl, session: sessionConfig, -} = require("../../../loadenv"); -const logger = require("../logger"); +} = require("@/loadenv"); +const logger = require("@/modules/logger"); const getSessionStore = (redisUrl) => { // 환경변수 REDIS_PATH 유무에 따라 session 저장 방식이 변경됩니다. diff --git a/src/routes/admin.js b/src/routes/admin.js index e2ddaae4..a621db61 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -12,15 +12,15 @@ const { adminLogModel, deviceTokenModel, notificationOptionModel, -} = require("../modules/stores/mongo"); -const { eventConfig } = require("../../loadenv"); -const { buildResource } = require("../modules/adminResource"); +} = require("@/modules/stores/mongo"); +const { eventConfig } = require("@/loadenv"); +const { buildResource } = require("@/modules/adminResource"); const router = express.Router(); // Requires admin property of the user to enter admin page. -router.use(require("../middlewares/authAdmin")); -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/authAdmin")); +router.use(require("@/middlewares/auth")); // Registration of the mongoose adapter AdminJS.registerAdapter(AdminJSMongoose); diff --git a/src/routes/auth.js b/src/routes/auth.js index 7b29a267..9ce57404 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,14 +1,14 @@ const express = require("express"); const router = express.Router(); const { body, query } = require("express-validator"); -const validator = require("../middlewares/validator"); +const validator = require("@/middlewares/validator"); -const authHandlers = require("../services/auth"); -const authReplaceHandlers = require("../services/auth.replace"); -const mobileAuthHandlers = require("../services/auth.mobile"); +const authHandlers = require("@/services/auth"); +const authReplaceHandlers = require("@/services/auth.replace"); +const mobileAuthHandlers = require("@/services/auth.mobile"); // 환경변수 SPARCSSSO_CLIENT_ID 유무에 따라 로그인 방식이 변경됩니다. -const { sparcssso: sparcsssoEnv } = require("../../loadenv"); +const { sparcssso: sparcsssoEnv } = require("@/loadenv"); const isAuthReplace = !sparcsssoEnv?.id; // 로그인 페이지로 redirect합니다. diff --git a/src/routes/chats.js b/src/routes/chats.js index f689348c..f27ace38 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -1,13 +1,13 @@ const express = require("express"); const { body } = require("express-validator"); -const validator = require("../middlewares/validator"); -const patterns = require("../modules/patterns"); +const validator = require("@/middlewares/validator"); +const patterns = require("@/modules/patterns"); const router = express.Router(); -const chatsHandlers = require("../services/chats"); +const chatsHandlers = require("@/services/chats"); // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth")); /** * 가장 최근에 도착한 60개의 채팅을 가져옵니다. diff --git a/src/routes/locations.js b/src/routes/locations.js index 7724d371..20b16ada 100644 --- a/src/routes/locations.js +++ b/src/routes/locations.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const locationsHandlers = require("../services/locations"); +const locationsHandlers = require("@/services/locations"); router.get("/", locationsHandlers.getAllLocationsHandler); diff --git a/src/routes/logininfo.js b/src/routes/logininfo.js index a3d9d4f7..ab8c32e9 100644 --- a/src/routes/logininfo.js +++ b/src/routes/logininfo.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const logininfoHandlers = require("../services/logininfo"); +const logininfoHandlers = require("@/services/logininfo"); router.route("/").get(logininfoHandlers.logininfoHandler); diff --git a/src/routes/notifications.js b/src/routes/notifications.js index dd666953..89ec5be9 100644 --- a/src/routes/notifications.js +++ b/src/routes/notifications.js @@ -1,12 +1,12 @@ const express = require("express"); const router = express.Router(); -const { query, body } = require("express-validator"); +const { body } = require("express-validator"); -const notificationHandlers = require("../services/notifications"); -const validator = require("../middlewares/validator"); +const notificationHandlers = require("@/services/notifications"); +const validator = require("@/middlewares/validator"); // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth")); // FCM 토큰 등록 router.post( diff --git a/src/routes/reports.js b/src/routes/reports.js index 07fcbf51..9c1f79d7 100644 --- a/src/routes/reports.js +++ b/src/routes/reports.js @@ -1,11 +1,11 @@ const express = require("express"); const reportsSchema = require("./docs/reportsSchema"); -const { validateBody } = require("../middlewares/ajv"); +const { validateBody } = require("@/middlewares/ajv"); const router = express.Router(); -const reportHandlers = require("../services/reports"); +const reportHandlers = require("@/services/reports"); // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth")); router.post( "/create", diff --git a/src/routes/rooms.js b/src/routes/rooms.js index be1c3f59..240b2f74 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -2,9 +2,9 @@ const express = require("express"); const { query, body } = require("express-validator"); const router = express.Router(); -const roomHandlers = require("../services/rooms"); -const validator = require("../middlewares/validator"); -const patterns = require("../modules/patterns"); +const roomHandlers = require("@/services/rooms"); +const validator = require("@/middlewares/validator"); +const patterns = require("@/modules/patterns"); // 조건(이름, 출발지, 도착지, 날짜)에 맞는 방들을 모두 반환한다. router.get( diff --git a/src/routes/users.js b/src/routes/users.js index 31bde597..9bc1d4eb 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -1,15 +1,15 @@ const express = require("express"); const { body } = require("express-validator"); -const validator = require("../middlewares/validator"); -const patterns = require("../modules/patterns"); +const validator = require("@/middlewares/validator"); +const patterns = require("@/modules/patterns"); const router = express.Router(); -const userHandlers = require("../services/users"); +const userHandlers = require("@/services/users"); -const { replaceSpaceInNickname } = require("../modules/modifyProfile"); +const { replaceSpaceInNickname } = require("@/modules/modifyProfile"); // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth")); // 이용 약관에 동의합니다. router.post( diff --git a/src/schedules/notifyAfterArrival.js b/src/schedules/notifyAfterArrival.js index 3202af08..5c1a5a6c 100644 --- a/src/schedules/notifyAfterArrival.js +++ b/src/schedules/notifyAfterArrival.js @@ -1,7 +1,7 @@ -const { roomModel, chatModel } = require("../modules/stores/mongo"); -// const { roomPopulateOption } = require("../modules/populates/rooms"); -const { emitChatEvent } = require("../modules/socket"); -const logger = require("../modules/logger"); +const { roomModel, chatModel } = require("@/modules/stores/mongo"); +// const { roomPopulateOption } = require("@/modules/populates/rooms"); +const { emitChatEvent } = require("@/modules/socket"); +const logger = require("@/modules/logger"); const MS_PER_MINUTE = 60000; diff --git a/src/schedules/notifyBeforeDepart.js b/src/schedules/notifyBeforeDepart.js index ffe66386..523f6dff 100644 --- a/src/schedules/notifyBeforeDepart.js +++ b/src/schedules/notifyBeforeDepart.js @@ -1,6 +1,6 @@ -const { roomModel, chatModel } = require("../modules/stores/mongo"); -const { emitChatEvent } = require("../modules/socket"); -const logger = require("../modules/logger"); +const { roomModel, chatModel } = require("@/modules/stores/mongo"); +const { emitChatEvent } = require("@/modules/socket"); +const logger = require("@/modules/logger"); const MS_PER_MINUTE = 60000; diff --git a/src/services/auth.js b/src/services/auth.js index 83b598de..2169aeb6 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -1,23 +1,19 @@ -const { - sparcssso: sparcsssoEnv, - nodeEnv, - testAccounts, -} = require("../../loadenv"); -const { userModel } = require("../modules/stores/mongo"); -const { user: userPattern } = require("../modules/patterns"); -const { getLoginInfo, logout, login } = require("../modules/auths/login"); - -const { unregisterDeviceToken } = require("../modules/fcm"); +const { sparcssso: sparcsssoEnv, nodeEnv, testAccounts } = require("@/loadenv"); +const { userModel } = require("@/modules/stores/mongo"); +const { user: userPattern } = require("@/modules/patterns"); +const { getLoginInfo, logout, login } = require("@/modules/auths/login"); + +const { unregisterDeviceToken } = require("@/modules/fcm"); const { generateNickname, generateProfileImageUrl, getFullUsername, -} = require("../modules/modifyProfile"); -const jwt = require("../modules/auths/jwt"); -const logger = require("../modules/logger"); +} = require("@/modules/modifyProfile"); +const jwt = require("@/modules/auths/jwt"); +const logger = require("@/modules/logger"); // SPARCS SSO -const Client = require("../modules/auths/sparcssso"); +const Client = require("@/modules/auths/sparcssso"); const client = new Client(sparcsssoEnv?.id, sparcsssoEnv?.key); const transUserData = (userData) => { diff --git a/src/services/auth.mobile.js b/src/services/auth.mobile.js index 0e537b33..7dc2798a 100644 --- a/src/services/auth.mobile.js +++ b/src/services/auth.mobile.js @@ -1,14 +1,11 @@ -const { userModel } = require("../modules/stores/mongo"); -const { login } = require("../modules/auths/login"); +const { userModel } = require("@/modules/stores/mongo"); +const { login } = require("@/modules/auths/login"); -const { - registerDeviceToken, - unregisterDeviceToken, -} = require("../modules/fcm"); -const jwt = require("../modules/auths/jwt"); -const logger = require("../modules/logger"); +const { registerDeviceToken, unregisterDeviceToken } = require("@/modules/fcm"); +const jwt = require("@/modules/auths/jwt"); +const logger = require("@/modules/logger"); -const { TOKEN_EXPIRED, TOKEN_INVALID } = require("../../loadenv").jwt; +const { TOKEN_EXPIRED, TOKEN_INVALID } = require("@/loadenv").jwt; const tokenLoginHandler = async (req, res) => { const { accessToken, deviceToken } = req.query; diff --git a/src/services/auth.replace.js b/src/services/auth.replace.js index a433e6f6..a054ea43 100644 --- a/src/services/auth.replace.js +++ b/src/services/auth.replace.js @@ -1,16 +1,16 @@ -const { userModel } = require("../modules/stores/mongo"); -const { logout, login } = require("../modules/auths/login"); +const { userModel } = require("@/modules/stores/mongo"); +const { logout, login } = require("@/modules/auths/login"); -const { unregisterDeviceToken } = require("../modules/fcm"); +const { unregisterDeviceToken } = require("@/modules/fcm"); const { generateNickname, generateProfileImageUrl, -} = require("../modules/modifyProfile"); -const logger = require("../modules/logger"); -const jwt = require("../modules/auths/jwt"); +} = require("@/modules/modifyProfile"); +const logger = require("@/modules/logger"); +const jwt = require("@/modules/auths/jwt"); -const { registerDeviceTokenHandler, tryLogin } = require("../services/auth"); -const loginReplacePage = require("../views/loginReplacePage"); +const { registerDeviceTokenHandler, tryLogin } = require("@/services/auth"); +const loginReplacePage = require("@/views/loginReplacePage"); const createUserData = (id) => { const info = { diff --git a/src/services/chats.js b/src/services/chats.js index abcbd073..921c4946 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -1,13 +1,13 @@ -const { chatModel, userModel, roomModel } = require("../modules/stores/mongo"); -const { chatPopulateOption } = require("../modules/populates/chats"); -const { roomPopulateOption } = require("../modules/populates/rooms"); -const aws = require("../modules/stores/aws"); +const { chatModel, userModel, roomModel } = require("@/modules/stores/mongo"); +const { chatPopulateOption } = require("@/modules/populates/chats"); +const { roomPopulateOption } = require("@/modules/populates/rooms"); +const aws = require("@/modules/stores/aws"); const { transformChatsForRoom, emitChatEvent, emitUpdateEvent, -} = require("../modules/socket"); -const logger = require("../modules/logger"); +} = require("@/modules/socket"); +const logger = require("@/modules/logger"); const chatCount = 60; diff --git a/src/services/locations.js b/src/services/locations.js index 5b02042d..ed81ed43 100644 --- a/src/services/locations.js +++ b/src/services/locations.js @@ -1,5 +1,5 @@ -const { locationModel } = require("../modules/stores/mongo"); -const logger = require("../modules/logger"); +const { locationModel } = require("@/modules/stores/mongo"); +const logger = require("@/modules/logger"); const getAllLocationsHandler = async (_, res) => { try { diff --git a/src/services/logininfo.js b/src/services/logininfo.js index eebb09ff..b074d847 100644 --- a/src/services/logininfo.js +++ b/src/services/logininfo.js @@ -1,6 +1,6 @@ -const { userModel } = require("../modules/stores/mongo"); -const { getLoginInfo } = require("../modules/auths/login"); -const logger = require("../modules/logger"); +const { userModel } = require("@/modules/stores/mongo"); +const { getLoginInfo } = require("@/modules/auths/login"); +const logger = require("@/modules/logger"); const logininfoHandler = async (req, res) => { try { diff --git a/src/services/notifications.js b/src/services/notifications.js index 633f6739..7134fab5 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -1,8 +1,8 @@ -const { userModel } = require("../modules/stores/mongo"); -const { notificationOptionModel } = require("../modules/stores/mongo"); -const logger = require("../modules/logger"); +const { userModel } = require("@/modules/stores/mongo"); +const { notificationOptionModel } = require("@/modules/stores/mongo"); +const logger = require("@/modules/logger"); -const { registerDeviceToken, validateDeviceToken } = require("../modules/fcm"); +const { registerDeviceToken, validateDeviceToken } = require("@/modules/fcm"); // 이벤트 코드입니다. const { contracts } = require("../lottery"); diff --git a/src/services/reports.js b/src/services/reports.js index 0451b0cc..be981f4a 100644 --- a/src/services/reports.js +++ b/src/services/reports.js @@ -1,13 +1,9 @@ -const { - userModel, - reportModel, - roomModel, -} = require("../modules/stores/mongo"); -const { reportPopulateOption } = require("../modules/populates/reports"); -const { sendReportEmail } = require("../modules/stores/aws"); -const logger = require("../modules/logger"); -const emailPage = require("../views/emailNoSettlementPage"); -const { notifyToReportChannel } = require("../modules/slackNotification"); +const { userModel, reportModel, roomModel } = require("@/modules/stores/mongo"); +const { reportPopulateOption } = require("@/modules/populates/reports"); +const { sendReportEmail } = require("@/modules/stores/aws"); +const logger = require("@/modules/logger"); +const emailPage = require("@/views/emailNoSettlementPage"); +const { notifyToReportChannel } = require("@/modules/slackNotification"); const createHandler = async (req, res) => { try { diff --git a/src/services/rooms.js b/src/services/rooms.js index d4b7557e..fed1ae66 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -2,14 +2,14 @@ const { roomModel, locationModel, userModel, -} = require("../modules/stores/mongo"); -const { emitChatEvent } = require("../modules/socket"); -const logger = require("../modules/logger"); +} = require("@/modules/stores/mongo"); +const { emitChatEvent } = require("@/modules/socket"); +const logger = require("@/modules/logger"); const { roomPopulateOption, formatSettlement, getIsOver, -} = require("../modules/populates/rooms"); +} = require("@/modules/populates/rooms"); // 이벤트 코드입니다. const { contracts } = require("../lottery"); diff --git a/src/services/users.js b/src/services/users.js index 3c26b164..73f79dde 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,13 +1,13 @@ -const { userModel } = require("../modules/stores/mongo"); -const logger = require("../modules/logger"); -const aws = require("../modules/stores/aws"); +const { userModel } = require("@/modules/stores/mongo"); +const logger = require("@/modules/logger"); +const aws = require("@/modules/stores/aws"); // 이벤트 코드입니다. const { contracts } = require("../lottery"); const { generateNickname, generateProfileImageUrl, -} = require("../modules/modifyProfile"); +} = require("@/modules/modifyProfile"); const agreeOnTermsOfServiceHandler = async (req, res) => { try { diff --git a/src/views/emailPage.js b/src/views/emailPage.js index d99dafd1..d89f297a 100644 --- a/src/views/emailPage.js +++ b/src/views/emailPage.js @@ -1,4 +1,4 @@ -const { getS3Url } = require("../modules/stores/aws"); +const { getS3Url } = require("@/modules/stores/aws"); module.exports = ( title, From f8cbff856da398f6d8cdcecb72770ed463394bdf Mon Sep 17 00:00:00 2001 From: withsang Date: Tue, 28 Nov 2023 22:32:15 +0900 Subject: [PATCH 008/151] Add: add rules for typescript-eslint --- .eslintrc.cjs | 73 +++- .prettierignore | 2 - package.json | 10 +- pnpm-lock.yaml | 1080 ++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 1057 insertions(+), 108 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 37ae58c8..5812d98a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,15 +1,78 @@ module.exports = { env: { - commonjs: true, es2021: true, node: true, }, - extends: ["eslint:recommended", "prettier", "plugin:mocha/recommended"], + extends: [ + "plugin:@typescript-eslint/recommended", + "airbnb-base", + "airbnb-typescript/base", + "plugin:mocha/recommended", + "prettier", + ], + overrides: [ + { + env: { + node: true, + mocha: true, + }, + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script", + }, + }, + ], + parser: "@typescript-eslint/parser", parserOptions: { - ecmaVersion: 13, + ecmaVersion: "latest", + sourceType: "module", + project: "./tsconfig.json", }, + plugins: ["import", "@typescript-eslint", "mocha"], rules: { - "no-unused-vars": 1, - "mocha/no-mocha-arrows": 0, + "import/extensions": [ + "error", + "ignorePackages", + { + ts: "never", + }, + ], + "import/named": "error", + "import/no-extraneous-dependencies": [ + "error", + { + packageDir: "./", + }, + ], + "mocha/no-mocha-arrows": "off", + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["../*"], + message: + "Usage of relative parent imports is not allowed. Use path alias instead.", + }, + ], + }, + ], + radix: ["error", "as-needed"], + "@typescript-eslint/consistent-type-imports": [ + "error", + { + prefer: "type-imports", + }, + ], + }, + settings: { + "import/parsers": { + "@typescript-eslint/parser": [".ts"], + }, + "import/resolver": { + typescript: { + project: ["./tsconfig.json"], + }, + }, }, }; diff --git a/.prettierignore b/.prettierignore index 15e64178..e0ed084a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,4 @@ node_modules/ dist/ package.json tsconfig.json -.prettierrc.json -.eslintrc.cjs nodemon.json \ No newline at end of file diff --git a/package.json b/package.json index fd1270b2..1f74e2a1 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build": "tsc && tsc-alias", "clean": "rimraf dist/", "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node dist/index.js", - "lint": "npx eslint --fix .", + "lint": "pnpm eslint .", "sample": "cd sampleGenerator && npm start && cd .." }, "engines": { @@ -36,7 +36,6 @@ "cors": "^2.8.5", "cross-env": "^7.0.3", "dotenv": "^16.0.1", - "eslint-config-prettier": "^8.3.0", "express": "^4.17.1", "express-formidable": "^1.2.0", "express-rate-limit": "^7.1.0", @@ -62,8 +61,15 @@ "@types/express": "^4.17.21", "@types/node": "^20.9.0", "@types/node-cron": "^3.0.11", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", "chai": "^4.3.10", "eslint": "^8.22.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-mocha": "^10.1.0", "mocha": "^10.2.0", "nodemon": "^3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be691a0e..c08ab98a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,9 +50,6 @@ dependencies: dotenv: specifier: ^16.0.1 version: 16.3.1 - eslint-config-prettier: - specifier: ^8.3.0 - version: 8.3.0(eslint@8.22.0) express: specifier: ^4.17.1 version: 4.18.2 @@ -124,12 +121,33 @@ devDependencies: '@types/node-cron': specifier: ^3.0.11 version: 3.0.11 + '@typescript-eslint/eslint-plugin': + specifier: ^6.13.1 + version: 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/parser': + specifier: ^6.13.1 + version: 6.13.1(eslint@8.22.0)(typescript@5.2.2) chai: specifier: ^4.3.10 version: 4.3.10 eslint: specifier: ^8.22.0 version: 8.22.0 + eslint-config-airbnb-base: + specifier: ^15.0.0 + version: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-config-airbnb-typescript: + specifier: ^17.1.0 + version: 17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-config-prettier: + specifier: ^8.3.0 + version: 8.3.0(eslint@8.22.0) + eslint-import-resolver-typescript: + specifier: ^3.6.1 + version: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-plugin-import: + specifier: ^2.29.0 + version: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) eslint-plugin-mocha: specifier: ^10.1.0 version: 10.1.0(eslint@8.22.0) @@ -157,6 +175,7 @@ packages: /@aashutoshrathi/word-wrap@1.2.6: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + dev: true /@adminjs/design-system@3.1.8(@types/react@18.2.18)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11): resolution: {integrity: sha512-M0l8NXoHKFoJ9XLv6BkrgRPnE0hCYNYWVNiQKA4qOpzifB2LAPAViqQ36Qyxgz1mL9nnzl7OJpGlb8cHSrIajg==} @@ -816,10 +835,10 @@ packages: '@babel/helpers': 7.22.6 '@babel/parser': 7.22.7 '@babel/template': 7.22.5 - '@babel/traverse': 7.22.8 + '@babel/traverse': 7.22.8(supports-color@5.5.0) '@babel/types': 7.22.5 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -903,7 +922,7 @@ packages: '@babel/core': 7.22.9 '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9) '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.4 transitivePeerDependencies: @@ -1044,7 +1063,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.22.5 - '@babel/traverse': 7.22.8 + '@babel/traverse': 7.22.8(supports-color@5.5.0) '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color @@ -2080,24 +2099,6 @@ packages: '@babel/types': 7.22.5 dev: false - /@babel/traverse@7.22.8: - resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/generator': 7.22.9 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-function-name': 7.22.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.22.7 - '@babel/types': 7.22.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/traverse@7.22.8(supports-color@5.5.0): resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==} engines: {node: '>=6.9.0'} @@ -2262,12 +2263,27 @@ packages: resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} dev: false + /@eslint-community/eslint-utils@4.4.0(eslint@8.22.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.22.0 + eslint-visitor-keys: 3.4.2 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + /@eslint/eslintrc@1.4.1: resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -2277,6 +2293,7 @@ packages: strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color + dev: true /@fastify/busboy@1.2.1: resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} @@ -2476,16 +2493,19 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color + dev: true /@humanwhocodes/gitignore-to-minimatch@1.0.2: resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==} + dev: true /@humanwhocodes/object-schema@1.2.1: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true /@hypnosphi/create-react-context@0.3.1(prop-types@15.8.1)(react@18.2.0): resolution: {integrity: sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==} @@ -2575,10 +2595,12 @@ packages: dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 + dev: true /@nodelib/fs.stat@2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} + dev: true /@nodelib/fs.walk@1.2.8: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} @@ -2586,6 +2608,7 @@ packages: dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + dev: true /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -3806,6 +3829,14 @@ packages: /@types/http-errors@2.0.1: resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + /@types/jsonwebtoken@9.0.2: resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} dependencies: @@ -3912,6 +3943,10 @@ packages: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: false + /@types/semver@7.5.6: + resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + dev: true + /@types/send@0.17.1: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: @@ -3948,6 +3983,137 @@ packages: '@types/webidl-conversions': 7.0.0 dev: false + /@typescript-eslint/eslint-plugin@6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.13.1 + '@typescript-eslint/type-utils': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.13.1 + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.22.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.13.1(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.13.1 + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.13.1 + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.22.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@6.13.1: + resolution: {integrity: sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/visitor-keys': 6.13.1 + dev: true + + /@typescript-eslint/type-utils@6.13.1(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) + '@typescript-eslint/utils': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.22.0 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@6.13.1: + resolution: {integrity: sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.13.1(typescript@5.2.2): + resolution: {integrity: sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/visitor-keys': 6.13.1 + debug: 4.3.4(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.13.1(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.22.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.13.1 + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) + eslint: 8.22.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@6.13.1: + resolution: {integrity: sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.1 + eslint-visitor-keys: 3.4.2 + dev: true + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true @@ -4038,7 +4204,7 @@ packages: engines: {node: '>= 6.0.0'} requiresBuild: true dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -4070,6 +4236,7 @@ packages: fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 + dev: true /ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} @@ -4123,13 +4290,76 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + dev: true + /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} dev: false + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-string: 1.0.7 + dev: true + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + dev: true + + /array.prototype.findlastindex@1.2.3: + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + get-intrinsic: 1.2.2 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true /arrify@2.0.1: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} @@ -4173,7 +4403,6 @@ packages: /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: false /aws-sdk@2.1430.0: resolution: {integrity: sha512-827BjW9Q9NwUucZBHHU64dh96ihE857LC0ZOEub0C5wjxujoEqET0i4qJR7k+/wn8tFMNtWO0rIGCSGSmgrm5A==} @@ -4344,6 +4573,7 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 + dev: true /browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} @@ -4406,6 +4636,13 @@ packages: function-bind: 1.1.1 get-intrinsic: 1.2.1 + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -4626,6 +4863,10 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + dev: true + /connect-mongo@4.6.0(express-session@1.17.3)(mongodb@4.17.1): resolution: {integrity: sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==} engines: {node: '>=10'} @@ -4633,7 +4874,7 @@ packages: express-session: ^1.17.1 mongodb: ^4.1.0 dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) express-session: 1.17.3 kruptein: 3.0.6 mongodb: 4.17.1 @@ -4810,17 +5051,6 @@ packages: supports-color: 5.5.0 dev: true - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -4832,7 +5062,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 5.5.0 - dev: false /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -4873,6 +5102,23 @@ packages: clone: 1.0.4 dev: false + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: true + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -4909,12 +5155,21 @@ packages: engines: {node: '>=8'} dependencies: path-type: 4.0.0 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} dependencies: esutils: 2.0.3 + dev: true /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -4997,7 +5252,7 @@ packages: base64id: 2.0.0 cookie: 0.4.2 cors: 2.8.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) engine.io-parser: 5.2.1 ws: 8.11.0 transitivePeerDependencies: @@ -5006,6 +5261,14 @@ packages: - utf-8-validate dev: false + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + /ent@2.2.0: resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} requiresBuild: true @@ -5029,6 +5292,75 @@ packages: is-arrayish: 0.2.1 dev: false + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + internal-slot: 1.0.6 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: true + + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + has-tostringtag: 1.0.0 + hasown: 2.0.0 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.0 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -5072,54 +5404,185 @@ packages: dev: false optional: true - /eslint-config-prettier@8.3.0(eslint@8.22.0): - resolution: {integrity: sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==} - hasBin: true + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.0)(eslint@8.22.0): + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: - eslint: '>=7.0.0' + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 dependencies: + confusing-browser-globals: 1.0.11 eslint: 8.22.0 - dev: false + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + object.assign: 4.1.4 + object.entries: 1.1.7 + semver: 6.3.1 + dev: true - /eslint-plugin-mocha@10.1.0(eslint@8.22.0): - resolution: {integrity: sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==} - engines: {node: '>=14.0.0'} + /eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0): + resolution: {integrity: sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.13.0 || ^6.0.0 + '@typescript-eslint/parser': ^5.0.0 || ^6.0.0 + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + dependencies: + '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + eslint: 8.22.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + dev: true + + /eslint-config-prettier@8.3.0(eslint@8.22.0): + resolution: {integrity: sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==} + hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: eslint: 8.22.0 - eslint-utils: 3.0.0(eslint@8.22.0) - rambda: 7.5.0 dev: true - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 + debug: 3.2.7(supports-color@5.5.0) + is-core-module: 2.13.1 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: true - /eslint-utils@3.0.0(eslint@8.22.0): - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0): + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - eslint: '>=5' + eslint: '*' + eslint-plugin-import: '*' dependencies: + debug: 4.3.4(supports-color@5.5.0) + enhanced-resolve: 5.15.0 eslint: 8.22.0 - eslint-visitor-keys: 2.1.0 - - /eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - /eslint-visitor-keys@3.4.2: - resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + fast-glob: 3.3.1 + get-tsconfig: 4.7.2 + is-core-module: 2.13.1 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true - /eslint@8.22.0: - resolution: {integrity: sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + debug: 3.2.7(supports-color@5.5.0) + eslint: 8.22.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0) + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0): + resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@5.5.0) + doctrine: 2.1.0 + eslint: 8.22.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-mocha@10.1.0(eslint@8.22.0): + resolution: {integrity: sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==} + engines: {node: '>=14.0.0'} + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.22.0 + eslint-utils: 3.0.0(eslint@8.22.0) + rambda: 7.5.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@3.0.0(eslint@8.22.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.22.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.4.2: + resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /eslint@8.22.0: + resolution: {integrity: sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true dependencies: '@eslint/eslintrc': 1.4.1 '@humanwhocodes/config-array': 0.10.7 @@ -5127,7 +5590,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -5162,6 +5625,7 @@ packages: v8-compile-cache: 2.3.0 transitivePeerDependencies: - supports-color + dev: true /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} @@ -5184,12 +5648,14 @@ packages: engines: {node: '>=0.10'} dependencies: estraverse: 5.3.0 + dev: true /esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} dependencies: estraverse: 5.3.0 + dev: true /estraverse@4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} @@ -5328,9 +5794,11 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 + dev: true /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -5367,6 +5835,7 @@ packages: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 + dev: true /faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} @@ -5384,6 +5853,7 @@ packages: engines: {node: ^10.12.0 || >=12.0.0} dependencies: flat-cache: 3.0.4 + dev: true /file-stream-rotator@0.6.1: resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} @@ -5396,6 +5866,7 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 + dev: true /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} @@ -5438,6 +5909,7 @@ packages: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 + dev: true /firebase-admin@11.10.1: resolution: {integrity: sha512-atv1E6GbuvcvWaD3eHwrjeP5dAVs+EaHEJhu9CThMzPY6In8QYDiUR6tq5SwGl4SdA/GcAU0nhwWc/FSJsAzfQ==} @@ -5465,6 +5937,7 @@ packages: dependencies: flatted: 3.2.7 rimraf: 3.0.2 + dev: true /flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} @@ -5472,6 +5945,7 @@ packages: /flatted@3.2.7: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true /fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} @@ -5491,7 +5965,6 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 - dev: false /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} @@ -5546,9 +6019,26 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + functions-have-names: 1.2.3 + dev: true + /functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + /gaxios@5.1.3: resolution: {integrity: sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==} engines: {node: '>=12'} @@ -5603,17 +6093,41 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: true + + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 + dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 + dev: true /glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} @@ -5671,6 +6185,14 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -5682,6 +6204,7 @@ packages: ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 + dev: true /google-auth-library@8.9.0: resolution: {integrity: sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==} @@ -5744,16 +6267,18 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 - dev: false /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} requiresBuild: true - dev: false - optional: true /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true /gtoken@6.1.2: resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} @@ -5773,6 +6298,10 @@ packages: resolution: {integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==} dev: false + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -5781,6 +6310,11 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} @@ -5794,7 +6328,6 @@ packages: engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 - dev: false /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} @@ -5802,6 +6335,12 @@ packages: dependencies: function-bind: 1.1.1 + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -5846,7 +6385,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -5858,7 +6397,7 @@ packages: requiresBuild: true dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -5888,6 +6427,7 @@ packages: /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} + dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -5899,6 +6439,7 @@ packages: /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + dev: true /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -5909,6 +6450,15 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + hasown: 2.0.0 + side-channel: 1.0.4 + dev: true + /ip@2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} dev: false @@ -5926,6 +6476,14 @@ packages: has-tostringtag: 1.0.0 dev: false + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: false @@ -5934,6 +6492,12 @@ packages: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} dev: false + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -5941,6 +6505,14 @@ packages: binary-extensions: 2.2.0 dev: true + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + /is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -5951,13 +6523,24 @@ packages: /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - dev: false /is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} dependencies: has: 1.0.3 - dev: false + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true /is-extendable@1.0.1: resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} @@ -5969,6 +6552,7 @@ packages: /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + dev: true /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -5986,6 +6570,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 + dev: true /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} @@ -5996,9 +6581,22 @@ packages: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} dev: false + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + dev: true /is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} @@ -6018,6 +6616,20 @@ packages: '@types/estree': 0.0.39 dev: false + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.5 + dev: true + /is-stream-ended@0.1.4: resolution: {integrity: sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==} requiresBuild: true @@ -6029,21 +6641,44 @@ packages: engines: {node: '>=8'} dev: false + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /is-typed-array@1.1.12: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} dependencies: - which-typed-array: 1.1.11 - dev: false + which-typed-array: 1.1.13 /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.5 + dev: true + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -6088,6 +6723,7 @@ packages: hasBin: true dependencies: argparse: 2.0.1 + dev: true /js2xmlparser@4.0.2: resolution: {integrity: sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==} @@ -6146,6 +6782,7 @@ packages: /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -6153,6 +6790,14 @@ packages: /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} @@ -6204,7 +6849,7 @@ packages: dependencies: '@types/express': 4.17.21 '@types/jsonwebtoken': 9.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) jose: 4.14.4 limiter: 1.1.5 lru-memoizer: 2.2.0 @@ -6273,6 +6918,7 @@ packages: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + dev: true /limiter@1.1.5: resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} @@ -6313,6 +6959,7 @@ packages: engines: {node: '>=10'} dependencies: p-locate: 5.0.0 + dev: true /lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -6358,6 +7005,7 @@ packages: /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true /lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -6543,6 +7191,7 @@ packages: /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + dev: true /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} @@ -6554,6 +7203,7 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 + dev: true /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} @@ -6625,8 +7275,6 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} requiresBuild: true - dev: false - optional: true /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} @@ -6719,7 +7367,7 @@ packages: resolution: {integrity: sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==} engines: {node: '>=12.0.0'} dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -6747,6 +7395,7 @@ packages: /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} @@ -6848,6 +7497,52 @@ packages: /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.groupby@1.0.1: + resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + dev: true + /object.omit@3.0.0: resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==} engines: {node: '>=0.10.0'} @@ -6862,6 +7557,15 @@ packages: isobject: 3.0.1 dev: false + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -6916,6 +7620,7 @@ packages: levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 + dev: true /ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} @@ -6961,6 +7666,7 @@ packages: engines: {node: '>=10'} dependencies: p-limit: 3.1.0 + dev: true /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} @@ -6996,6 +7702,7 @@ packages: /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + dev: true /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -7007,7 +7714,6 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: false /path-scurry@1.10.1: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} @@ -7086,6 +7792,7 @@ packages: /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + dev: true /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -7358,6 +8065,7 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true /raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} @@ -7659,9 +8367,19 @@ packages: '@babel/runtime': 7.22.6 dev: false + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + /regexpp@3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} + dev: true /regexpu-core@5.3.2: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} @@ -7703,6 +8421,10 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve@1.22.4: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true @@ -7710,7 +8432,6 @@ packages: is-core-module: 2.13.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: false /response-time@2.3.2: resolution: {integrity: sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==} @@ -7733,7 +8454,7 @@ packages: engines: {node: '>=12'} requiresBuild: true dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) extend: 3.0.2 transitivePeerDependencies: - supports-color @@ -7750,6 +8471,7 @@ packages: /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} @@ -7794,10 +8516,29 @@ packages: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-regex: 1.1.4 + dev: true + /safe-stable-stringify@2.4.3: resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} engines: {node: '>=10'} @@ -7825,7 +8566,6 @@ packages: /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - dev: false /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} @@ -7879,6 +8619,24 @@ packages: - supports-color dev: false + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.1 + dev: true + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -7960,7 +8718,7 @@ packages: engines: {node: '>=10.0.0'} dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -7972,7 +8730,7 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) engine.io: 6.5.2 socket.io-adapter: 2.5.2 socket.io-parser: 4.2.4 @@ -8060,6 +8818,31 @@ packages: strip-ansi: 7.1.0 dev: true + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -8079,6 +8862,11 @@ packages: ansi-regex: 6.0.1 dev: true + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -8146,7 +8934,7 @@ packages: dependencies: component-emitter: 1.3.0 cookiejar: 2.1.4 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 2.1.2 @@ -8190,7 +8978,6 @@ packages: /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: false /swagger-ui-dist@5.3.1: resolution: {integrity: sha512-El78OvXp9zMasfPrshtkW1CRx8AugAKoZuGGOTW+8llJzOV1RtDJYqQRz/6+2OakjeWWnZuRlN2Qj1Y0ilux3w==} @@ -8206,6 +8993,11 @@ packages: swagger-ui-dist: 5.3.1 dev: false + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + /teeny-request@8.0.3: resolution: {integrity: sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==} engines: {node: '>=12'} @@ -8247,6 +9039,7 @@ packages: /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true /throttle-debounce@3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} @@ -8282,6 +9075,7 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 + dev: true /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} @@ -8313,6 +9107,15 @@ packages: engines: {node: '>= 14.0.0'} dev: false + /ts-api-utils@1.0.3(typescript@5.2.2): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.2.2 + dev: true + /tsc-alias@1.8.8: resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==} hasBin: true @@ -8325,6 +9128,15 @@ packages: plimit-lit: 1.6.1 dev: true + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} requiresBuild: true @@ -8353,6 +9165,7 @@ packages: engines: {node: '>= 0.8.0'} dependencies: prelude-ls: 1.2.1 + dev: true /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} @@ -8362,6 +9175,7 @@ packages: /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + dev: true /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} @@ -8376,6 +9190,44 @@ packages: mime-types: 2.1.35 dev: false + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + /typescript@5.2.2: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} @@ -8401,6 +9253,15 @@ packages: random-bytes: 1.0.0 dev: false + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.5 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + /undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} dev: true @@ -8530,6 +9391,7 @@ packages: /v8-compile-cache@2.3.0: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} + dev: true /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} @@ -8604,6 +9466,16 @@ packages: dev: false optional: true + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + /which-typed-array@1.1.11: resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} engines: {node: '>= 0.4'} @@ -8615,6 +9487,16 @@ packages: has-tostringtag: 1.0.0 dev: false + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} From e1155abc44d4841744c83501c342aae2696011da Mon Sep 17 00:00:00 2001 From: withsang Date: Tue, 28 Nov 2023 23:40:52 +0900 Subject: [PATCH 009/151] Add: type definitions for some middlewares --- .eslintrc.cjs | 2 + package.json | 3 ++ pnpm-lock.yaml | 29 ++++++++++-- src/loadenv.ts | 18 ++++--- src/middlewares/{auth.js => auth.ts} | 8 ++-- .../{authAdmin.js => authAdmin.ts} | 18 ++++--- src/middlewares/cors.js | 8 ---- src/middlewares/cors.ts | 10 ++++ .../{errorHandler.js => errorHandler.ts} | 22 +++++---- src/middlewares/information.js | 5 -- src/middlewares/information.ts | 13 +++++ .../{limitRate.js => limitRate.ts} | 4 +- ...{originValidator.js => originValidator.ts} | 12 ++++- src/middlewares/responseTime.js | 11 ----- src/middlewares/responseTime.ts | 16 +++++++ src/middlewares/session.js | 14 ------ src/middlewares/session.ts | 40 ++++++++++++++++ src/modules/auths/jwt.js | 43 ----------------- src/modules/auths/jwt.ts | 47 +++++++++++++++++++ src/modules/auths/{login.js => login.ts} | 36 ++++++++------ src/modules/{logger.js => logger.ts} | 10 ++-- tsconfig.json | 4 +- types/index.d.ts | 19 ++++++++ 23 files changed, 256 insertions(+), 136 deletions(-) rename src/middlewares/{auth.js => auth.ts} (55%) rename src/middlewares/{authAdmin.js => authAdmin.ts} (63%) delete mode 100644 src/middlewares/cors.js create mode 100644 src/middlewares/cors.ts rename src/middlewares/{errorHandler.js => errorHandler.ts} (64%) delete mode 100644 src/middlewares/information.js create mode 100644 src/middlewares/information.ts rename src/middlewares/{limitRate.js => limitRate.ts} (80%) rename src/middlewares/{originValidator.js => originValidator.ts} (57%) delete mode 100644 src/middlewares/responseTime.js create mode 100644 src/middlewares/responseTime.ts delete mode 100644 src/middlewares/session.js create mode 100644 src/middlewares/session.ts delete mode 100644 src/modules/auths/jwt.js create mode 100644 src/modules/auths/jwt.ts rename src/modules/auths/{login.js => login.ts} (65%) rename src/modules/{logger.js => logger.ts} (93%) create mode 100644 types/index.d.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5812d98a..6f4e2b8c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -34,6 +34,7 @@ module.exports = { "error", "ignorePackages", { + js: "never", // temporary fix for #159 ts: "never", }, ], @@ -45,6 +46,7 @@ module.exports = { }, ], "mocha/no-mocha-arrows": "off", + "no-console": "error", "no-restricted-imports": [ "error", { diff --git a/package.json b/package.json index 1f74e2a1..d0a3d172 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,11 @@ "@types/cookie-parser": "^1.4.6", "@types/cors": "^2.8.16", "@types/express": "^4.17.21", + "@types/express-session": "^1.17.10", + "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.9.0", "@types/node-cron": "^3.0.11", + "@types/response-time": "^2.3.8", "@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/parser": "^6.13.1", "chai": "^4.3.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c08ab98a..72a2f835 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,12 +115,21 @@ devDependencies: '@types/express': specifier: ^4.17.21 version: 4.17.21 + '@types/express-session': + specifier: ^1.17.10 + version: 1.17.10 + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.5 '@types/node': specifier: ^20.9.0 version: 20.9.0 '@types/node-cron': specifier: ^3.0.11 version: 3.0.11 + '@types/response-time': + specifier: ^2.3.8 + version: 2.3.8 '@typescript-eslint/eslint-plugin': specifier: ^6.13.1 version: 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2) @@ -3802,6 +3811,12 @@ packages: '@types/range-parser': 1.2.4 '@types/send': 0.17.1 + /@types/express-session@1.17.10: + resolution: {integrity: sha512-U32bC/s0ejXijw5MAzyaV4tuZopCh/K7fPoUDyNbsRXHvPSeymygYD1RFL99YOLhF5PNOkzswvOTRaVHdL1zMw==} + dependencies: + '@types/express': 4.17.21 + dev: true + /@types/express@4.17.21: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: @@ -3837,11 +3852,10 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/jsonwebtoken@9.0.2: - resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} dependencies: '@types/node': 20.9.0 - dev: false /@types/linkify-it@3.0.2: resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} @@ -3930,6 +3944,13 @@ packages: '@types/node': 20.9.0 dev: false + /@types/response-time@2.3.8: + resolution: {integrity: sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==} + dependencies: + '@types/express': 4.17.21 + '@types/node': 20.9.0 + dev: true + /@types/rimraf@3.0.2: resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} requiresBuild: true @@ -6848,7 +6869,7 @@ packages: engines: {node: '>=14'} dependencies: '@types/express': 4.17.21 - '@types/jsonwebtoken': 9.0.2 + '@types/jsonwebtoken': 9.0.5 debug: 4.3.4(supports-color@5.5.0) jose: 4.14.4 limiter: 1.1.5 diff --git a/src/loadenv.ts b/src/loadenv.ts index 6307bdca..15d6bf08 100644 --- a/src/loadenv.ts +++ b/src/loadenv.ts @@ -1,8 +1,11 @@ // 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옵니다. import dotenv from "dotenv"; +import { type Algorithm } from "jsonwebtoken"; +import { type ServiceAccount } from "firebase-admin"; if (process.env.NODE_ENV === undefined) { // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. + // eslint-disable-next-line no-console console.error("NODE_ENV 환경변수가 설정되어 있지 않습니다."); process.exit(1); } @@ -10,6 +13,7 @@ dotenv.config({ path: `./.env.${process.env.NODE_ENV}` }); if (process.env.DB_PATH === undefined) { // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. + // eslint-disable-next-line no-console console.error("DB_PATH 환경변수가 설정되어 있지 않습니다."); process.exit(1); } @@ -27,11 +31,11 @@ export const sparcssso = { }; export const port = process.env.PORT ? parseInt(process.env.PORT) : 80; // optional (default = 80) export const corsWhiteList = (process.env.CORS_WHITELIST && - JSON.parse(process.env.CORS_WHITELIST)) || [true]; // optional (default = [true]) + JSON.parse(process.env.CORS_WHITELIST) as string[]) || [true]; // optional (default = [true]) export const aws = { - accessKeyId: process.env.AWS_ACCESS_KEY_ID, // required - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // required - s3BucketName: process.env.AWS_S3_BUCKET_NAME, // required + accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, // required + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, // required + s3BucketName: process.env.AWS_S3_BUCKET_NAME as string, // required s3Url: process.env.AWS_S3_URL || `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional @@ -39,7 +43,7 @@ export const aws = { export const jwt = { secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", option: { - algorithm: "HS256", + algorithm: "HS256" as Algorithm, // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. // See https://github.com/sparcs-kaist/taxi-back/issues/415 issuer: process.env.FRONT_URL || "http://localhost:3000", // optional (default = "http://localhost:3000") @@ -49,9 +53,9 @@ export const jwt = { }; export const googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS && - JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS); // optional + JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS) as ServiceAccount; // optional export const testAccounts = - (process.env.TEST_ACCOUNTS && JSON.parse(process.env.TEST_ACCOUNTS)) || []; // optional + (process.env.TEST_ACCOUNTS && JSON.parse(process.env.TEST_ACCOUNTS) as string[]) || []; // optional export const slackWebhookUrl = { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }; diff --git a/src/middlewares/auth.js b/src/middlewares/auth.ts similarity index 55% rename from src/middlewares/auth.js rename to src/middlewares/auth.ts index 15d1f5a5..535ed1ec 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.ts @@ -1,8 +1,8 @@ // 로그인된 상태에만 접근할 수 있는 라우터(rooms)를 위한 미들웨어입니다. +import { type Request, type Response, type NextFunction } from "express"; +import { isLogin, getLoginInfo } from "@/modules/auths/login"; -const { isLogin, getLoginInfo } = require("@/modules/auths/login"); - -const authMiddleware = (req, res, next) => { +const authMiddleware = (req: Request, res: Response, next: NextFunction) => { if (!isLogin(req)) { res.status(403).json({ error: "not logged in", @@ -15,4 +15,4 @@ const authMiddleware = (req, res, next) => { } }; -module.exports = authMiddleware; +export default authMiddleware; diff --git a/src/middlewares/authAdmin.js b/src/middlewares/authAdmin.ts similarity index 63% rename from src/middlewares/authAdmin.js rename to src/middlewares/authAdmin.ts index 34fc5df6..75c67083 100644 --- a/src/middlewares/authAdmin.js +++ b/src/middlewares/authAdmin.ts @@ -1,9 +1,13 @@ // 관리자 유무를 확인하기 위한 미들웨어입니다. +import { type Request, type Response, type NextFunction } from "express"; +import { isLogin, getLoginInfo } from "@/modules/auths/login"; +import { userModel, adminIPWhitelistModel } from "@/modules/stores/mongo"; -const { isLogin, getLoginInfo } = require("@/modules/auths/login"); -const { userModel, adminIPWhitelistModel } = require("@/modules/stores/mongo"); - -const authAdminMiddleware = async (req, res, next) => { +const authAdminMiddleware = async ( + req: Request, + res: Response, + next: NextFunction +) => { try { // 로그인 여부를 확인 if (!isLogin(req)) return res.redirect(req.origin); @@ -22,10 +26,10 @@ const authAdminMiddleware = async (req, res, next) => { ) return res.redirect(req.origin); - next(); + return next(); } catch (e) { - res.redirect(req.origin); + return res.redirect(req.origin); } }; -module.exports = authAdminMiddleware; +export default authAdminMiddleware; diff --git a/src/middlewares/cors.js b/src/middlewares/cors.js deleted file mode 100644 index a1012607..00000000 --- a/src/middlewares/cors.js +++ /dev/null @@ -1,8 +0,0 @@ -var cors = require("cors"); -const { corsWhiteList } = require("@/loadenv"); - -module.exports = cors({ - origin: corsWhiteList, - credentials: true, - exposedHeaders: ["Date"], -}); diff --git a/src/middlewares/cors.ts b/src/middlewares/cors.ts new file mode 100644 index 00000000..63559565 --- /dev/null +++ b/src/middlewares/cors.ts @@ -0,0 +1,10 @@ +import cors from "cors"; +import { corsWhiteList } from "@/loadenv"; + +const corsMiddleware = cors({ + origin: corsWhiteList, + credentials: true, + exposedHeaders: ["Date"], +}); + +export default corsMiddleware; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.ts similarity index 64% rename from src/middlewares/errorHandler.js rename to src/middlewares/errorHandler.ts index d9bdcc0c..1b8c26ca 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.ts @@ -1,16 +1,22 @@ -const logger = require("@/modules/logger"); +import logger from "@/modules/logger"; +import { type Request, type Response, type NextFunction } from "express"; /** * Express app에서 사용할 custom global error handler를 정의합니다. * @summary Express 핸들러에서 발생한 uncaught exception은 이 핸들러를 통해 처리됩니다. * Express에서 제공하는 기본 global error handler는 클라이언트에 오류 발생 call stack을 그대로 반환합니다. * 이 때문에 클라이언트에게 잠재적으로 보안 취약점을 노출할 수 있으므로, call stack을 반환하지 않는 error handler를 정의합니다. - * @param {Error} err - 오류 객체 - * @param {Express.Request} req - 요청 객체 - * @param {Express.Response} res - 응답 객체 - * @param {Function} next - 다음 미들웨어 함수. Express에서는 next 함수에 err 인자를 넘겨주면 기본 global error handler가 호출됩니다. + * @param err - 오류 객체 + * @param req - 요청 객체 + * @param res - 응답 객체 + * @param next - 다음 미들웨어 함수. Express에서는 next 함수에 err 인자를 넘겨주면 기본 global error handler가 호출됩니다. */ -const errorHandler = (err, req, res, next) => { +const errorHandler = ( + err: Error, + req: Request, + res: Response, + next: NextFunction +) => { // 이미 클라이언트에 HTTP 응답 헤더를 전송한 경우, 응답 헤더를 다시 전송하지 않아야 합니다. // 클라이언트에게 스트리밍 형태로 응답을 전송하는 도중 오류가 발생하는 경우가 여기에 해당합니다. // 이럴 때 기본 global error handler를 호출하면 기본 global error handler가 클라이언트와의 연결을 종료시켜 줍니다. @@ -18,7 +24,7 @@ const errorHandler = (err, req, res, next) => { if (res.headersSent) { return next(err); } - res.status(500).send("internal server error"); + return res.status(500).send("internal server error"); }; -module.exports = errorHandler; +export default errorHandler; diff --git a/src/middlewares/information.js b/src/middlewares/information.js deleted file mode 100644 index f7197d1f..00000000 --- a/src/middlewares/information.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = (req, res, next) => { - req.clientIP = req.headers["x-forwarded-for"] || req.connection.remoteAddress; - req.timestamp = Date.now(); - next(); -}; diff --git a/src/middlewares/information.ts b/src/middlewares/information.ts new file mode 100644 index 00000000..3d2cd606 --- /dev/null +++ b/src/middlewares/information.ts @@ -0,0 +1,13 @@ +import { type Request, type Response, type NextFunction } from "express"; + +const informationMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + req.clientIP = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + req.timestamp = Date.now(); + next(); +}; + +export default informationMiddleware; diff --git a/src/middlewares/limitRate.js b/src/middlewares/limitRate.ts similarity index 80% rename from src/middlewares/limitRate.js rename to src/middlewares/limitRate.ts index 4cba6af3..d137892b 100644 --- a/src/middlewares/limitRate.js +++ b/src/middlewares/limitRate.ts @@ -1,4 +1,4 @@ -const rateLimit = require("express-rate-limit"); +import rateLimit from "express-rate-limit"; const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes @@ -7,4 +7,4 @@ const limiter = rateLimit({ legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); -module.exports = limiter; +export default limiter; diff --git a/src/middlewares/originValidator.js b/src/middlewares/originValidator.ts similarity index 57% rename from src/middlewares/originValidator.js rename to src/middlewares/originValidator.ts index 2aec851d..3927d46e 100644 --- a/src/middlewares/originValidator.js +++ b/src/middlewares/originValidator.ts @@ -1,4 +1,10 @@ -module.exports = (req, res, next) => { +import { type Request, type Response, type NextFunction } from "express"; + +const originValidatorMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { req.origin = req.headers.origin || req.headers.referer || @@ -9,5 +15,7 @@ module.exports = (req, res, next) => { error: "Bad Request : request must have origin in header", }); } - next(); + return next(); }; + +export default originValidatorMiddleware; diff --git a/src/middlewares/responseTime.js b/src/middlewares/responseTime.js deleted file mode 100644 index c042ffb3..00000000 --- a/src/middlewares/responseTime.js +++ /dev/null @@ -1,11 +0,0 @@ -const logger = require("@/modules/logger"); -const responseTime = require("response-time"); - -module.exports = responseTime((req, res, time) => { - const { method, originalUrl, clientIP } = req; - const { statusCode } = res; - const userId = req.session?.loginInfo?.id || "anonymous"; - logger.info( - `${userId}(${clientIP}) "${method} ${originalUrl}" ${statusCode} on ${time}ms` - ); -}); diff --git a/src/middlewares/responseTime.ts b/src/middlewares/responseTime.ts new file mode 100644 index 00000000..aec93458 --- /dev/null +++ b/src/middlewares/responseTime.ts @@ -0,0 +1,16 @@ +import { type Request, type Response } from "express"; +import logger from "@/modules/logger"; +import responseTime from "response-time"; + +const responseTimeMiddleware = responseTime( + (req: Request, res: Response, time: number) => { + const { method, originalUrl, clientIP } = req; + const { statusCode } = res; + const userId = req.session?.loginInfo?.id || "anonymous"; + logger.info( + `${userId}(${clientIP}) "${method} ${originalUrl}" ${statusCode} on ${time}ms` + ); + } +); + +export default responseTimeMiddleware; diff --git a/src/middlewares/session.js b/src/middlewares/session.js deleted file mode 100644 index b4be3266..00000000 --- a/src/middlewares/session.js +++ /dev/null @@ -1,14 +0,0 @@ -const expressSession = require("express-session"); -const { nodeEnv, session: sessionConfig } = require("../../loadenv"); -const sessionStore = require("@/modules/stores/sessionStore"); - -module.exports = expressSession({ - secret: sessionConfig.secret, - resave: false, - saveUninitialized: false, - store: sessionStore, - cookie: { - maxAge: sessionConfig.expiry, - secure: nodeEnv === "production", - }, -}); diff --git a/src/middlewares/session.ts b/src/middlewares/session.ts new file mode 100644 index 00000000..5a49e208 --- /dev/null +++ b/src/middlewares/session.ts @@ -0,0 +1,40 @@ +import expressSession from "express-session"; +import { nodeEnv, session as sessionConfig } from "@/loadenv"; +import sessionStore from "@/modules/stores/sessionStore"; +import { type LoginInfo } from "@/modules/auths/login"; + +// 세션에 저장할 데이터 타입을 지정합니다. +declare module 'express-session' { + interface SessionData { + /** 사용자 로그인 정보 */ + loginInfo?: LoginInfo; + /** 현재 로그인된 사용자가 앱으로 접속했는지 여부 */ + isApp?: boolean; + /** SPARCS SSO 로그인 시 state와 로그인 후 redirect 주소를 저장할 object. 타입 수정 필요. */ + loginAfterState?: { + state?: string; + redirectOrigin?: string; + redirectPath?: string; + }; + /** 앱 로그인용 access token */ + accessToken?: string; + /** 앱 로그인용 refresh token */ + refreshToken?: string; + /** FCM용 device token */ + deviceToken?: string; + } +} + +const sessionMiddleware = expressSession({ + secret: sessionConfig.secret, + resave: false, + saveUninitialized: false, + store: sessionStore, + cookie: { + maxAge: sessionConfig.expiry, + // nodeEnv가 production일 때만 secure cookie를 사용합니다. + secure: nodeEnv === "production", + }, +}); + +export default sessionMiddleware; \ No newline at end of file diff --git a/src/modules/auths/jwt.js b/src/modules/auths/jwt.js deleted file mode 100644 index 4e8f7259..00000000 --- a/src/modules/auths/jwt.js +++ /dev/null @@ -1,43 +0,0 @@ -const jwt = require("jsonwebtoken"); -const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = - require("@/loadenv").jwt; - -const signJwt = async ({ id, type }) => { - const payload = { - id: id, - type: type, - }; - - const options = { ...option }; - - if (type === "refresh") { - options.expiresIn = "30d"; - } - if (type === "access") { - options.expiresIn = "14d"; - } - - const result = { - token: jwt.sign(payload, secretKey, options), - }; - return result; -}; - -const verifyJwt = async (token) => { - let decoded; - try { - decoded = jwt.verify(token, secretKey); - } catch (err) { - if (err.message === "jwt expired") { - return TOKEN_EXPIRED; - } else { - return TOKEN_INVALID; - } - } - return decoded; -}; - -module.exports = { - sign: signJwt, - verify: verifyJwt, -}; diff --git a/src/modules/auths/jwt.ts b/src/modules/auths/jwt.ts new file mode 100644 index 00000000..ee009330 --- /dev/null +++ b/src/modules/auths/jwt.ts @@ -0,0 +1,47 @@ +import jwt, { type SignOptions } from "jsonwebtoken"; +import { jwt as jwtConfig } from "@/loadenv"; + +const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = jwtConfig; + +type TokenType = "access" | "refresh"; + +interface SignType { + id: string; + type: TokenType; +} + +export const sign = async ({ id, type }: SignType) => { + const payload = { + id, + type, + }; + + const options: SignOptions = { ...option }; + + if (type === "refresh") { + options.expiresIn = "30d"; + } + if (type === "access") { + options.expiresIn = "14d"; + } + + const result = { + token: jwt.sign(payload, secretKey, options), + }; + return result; +}; + +export const verify = async (token: string) => { + let decoded; + try { + decoded = jwt.verify(token, secretKey); + } catch (err) { + if (err instanceof Error) { + if (err.message === "jwt expired") { + return TOKEN_EXPIRED; + } + } + return TOKEN_INVALID; + } + return decoded; +}; diff --git a/src/modules/auths/login.js b/src/modules/auths/login.ts similarity index 65% rename from src/modules/auths/login.js rename to src/modules/auths/login.ts index 7c4f6baf..a55677b6 100644 --- a/src/modules/auths/login.js +++ b/src/modules/auths/login.ts @@ -1,7 +1,16 @@ -const { session: sessionConfig } = require("@/loadenv"); -const logger = require("@/modules/logger"); +import { type Request } from "express"; +import { session as sessionConfig } from "@/loadenv"; +import logger from "@/modules/logger"; -const getLoginInfo = (req) => { +export interface LoginInfo { + id: string; + sid: string; + oid: string; + name: string; + time: number; +} + +export const getLoginInfo = (req: Request) => { if (req.session.loginInfo) { const { id, sid, oid, name, time } = req.session.loginInfo; const timeFlow = Date.now() - time; @@ -15,17 +24,23 @@ const getLoginInfo = (req) => { return { id: undefined, sid: undefined, oid: undefined, name: undefined }; }; -const isLogin = (req) => { +export const isLogin = (req: Request) => { const loginInfo = getLoginInfo(req); if (loginInfo.id) return true; - else return false; + return false; }; -const login = (req, sid, id, oid, name) => { +export const login = ( + req: Request, + sid: string, + id: string, + oid: string, + name: string +) => { req.session.loginInfo = { sid, id, oid, name, time: Date.now() }; }; -const logout = (req) => { +export const logout = (req: Request) => { // 로그아웃 전 socket.io 소켓들 연결부터 끊기 const io = req.app.get("io"); if (io) io.in(`session-${req.session.id}`).disconnectSockets(true); @@ -34,10 +49,3 @@ const logout = (req) => { if (err) logger.error(err); }); }; - -module.exports = { - getLoginInfo, - isLogin, - login, - logout, -}; diff --git a/src/modules/logger.js b/src/modules/logger.ts similarity index 93% rename from src/modules/logger.js rename to src/modules/logger.ts index 3baa80d5..b4b91610 100644 --- a/src/modules/logger.js +++ b/src/modules/logger.ts @@ -1,8 +1,8 @@ -const path = require("path"); -const { createLogger, format, transports } = require("winston"); -const DailyRotateFileTransport = require("winston-daily-rotate-file"); +import path from "path"; +import { createLogger, format, transports } from "winston"; +import DailyRotateFileTransport from "winston-daily-rotate-file"; -const { nodeEnv } = require("@/loadenv"); +import { nodeEnv } from "@/loadenv"; // logger에서 사용할 포맷들을 정의합니다. const baseFormat = format.combine( @@ -92,4 +92,4 @@ const logger = exceptionHandlers: [consoleTransport], }); -module.exports = logger; +export default logger; diff --git a/tsconfig.json b/tsconfig.json index 415e2140..0e7f68ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "@/*": ["./*"] } }, - "include": ["src"], - "exclude": ["dist", "node_modules", "src/lottery"] + "include": ["src", "types"], + "exclude": ["dist", "node_modules"] } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..acee1ace --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,19 @@ +// to make the file a module and avoid the TypeScript error +export {}; + +declare global { + namespace Express { + export interface Request { + /** 사용자 ID. SPARCS SSO로부터 전달받습니다. */ + userId?: string; + /** 사용자의 ObjectID. MongoDB에서 사용됩니다. */ + userOid?: string; + /** 요청의 origin. */ + origin?: string; + /** 사용자의 IP 주소. */ + clientIP?: string; + /** 요청의 timestamp. */ + timestamp?: number; + } + } +} \ No newline at end of file From 052d144e70adcff4ed7b2d8bf6f2b74acb0e4078 Mon Sep 17 00:00:00 2001 From: withsang Date: Wed, 29 Nov 2023 00:05:48 +0900 Subject: [PATCH 010/151] Add: type definitions for templates --- src/middlewares/validator.js | 11 ----------- src/middlewares/validator.ts | 18 ++++++++++++++++++ src/schedules/index.ts | 8 +++++--- ...tlementPage.js => emailNoSettlementPage.ts} | 13 +++++++++++-- src/views/{emailPage.js => emailPage.ts} | 10 ++++++---- ...loginReplacePage.js => loginReplacePage.ts} | 4 +++- 6 files changed, 43 insertions(+), 21 deletions(-) delete mode 100644 src/middlewares/validator.js create mode 100644 src/middlewares/validator.ts rename src/views/{emailNoSettlementPage.js => emailNoSettlementPage.ts} (88%) rename src/views/{emailPage.js => emailPage.ts} (92%) rename src/views/{loginReplacePage.js => loginReplacePage.ts} (97%) diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js deleted file mode 100644 index 8bdeb4c4..00000000 --- a/src/middlewares/validator.js +++ /dev/null @@ -1,11 +0,0 @@ -const { validationResult } = require("express-validator"); - -module.exports = (req, res, next) => { - const validationErrors = validationResult(req); - if (!validationErrors.isEmpty()) { - return res.status(400).json({ - error: "validation : bad request", - }); - } - next(); -}; diff --git a/src/middlewares/validator.ts b/src/middlewares/validator.ts new file mode 100644 index 00000000..b4e1b0cf --- /dev/null +++ b/src/middlewares/validator.ts @@ -0,0 +1,18 @@ +import { type Request, type Response, type NextFunction } from "express"; +import { validationResult } from "express-validator"; + +const validatorMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + const validationErrors = validationResult(req); + if (!validationErrors.isEmpty()) { + return res.status(400).json({ + error: "validation : bad request", + }); + } + return next(); +}; + +export default validatorMiddleware; diff --git a/src/schedules/index.ts b/src/schedules/index.ts index f57ca9dc..f9e62477 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -1,9 +1,11 @@ -import { Express } from "express"; +import { type Express } from "express"; import cron from "node-cron"; +import notifyBeforeDepart from "./notifyBeforeDepart"; +import notifyAfterArrival from "./notifyAfterArrival"; const registerSchedules = (app: Express) => { - cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); - cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); + cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); + cron.schedule("*/10 * * * *", notifyAfterArrival(app)); }; export default registerSchedules; diff --git a/src/views/emailNoSettlementPage.js b/src/views/emailNoSettlementPage.ts similarity index 88% rename from src/views/emailNoSettlementPage.js rename to src/views/emailNoSettlementPage.ts index 5143c279..b9bd3ece 100644 --- a/src/views/emailNoSettlementPage.js +++ b/src/views/emailNoSettlementPage.ts @@ -1,6 +1,13 @@ -const emailPage = require("./emailPage"); +import emailPage from "./emailPage"; -module.exports = (origin, name, nickname, roomName, payer, roomId) => +const emailNoSettlementPage = ( + origin: string, + name: string, + nickname: string, + roomName: string, + payer: string, + roomId: string +) => emailPage( "미정산 내역 관련 안내", `${name} (${nickname}) 님께

@@ -32,3 +39,5 @@ module.exports = (origin, name, nickname, roomName, payer, roomId) => SPARCS Taxi팀 드림. ` ); + +export default emailNoSettlementPage; diff --git a/src/views/emailPage.js b/src/views/emailPage.ts similarity index 92% rename from src/views/emailPage.js rename to src/views/emailPage.ts index d89f297a..b6b1a145 100644 --- a/src/views/emailPage.js +++ b/src/views/emailPage.ts @@ -1,8 +1,8 @@ -const { getS3Url } = require("@/modules/stores/aws"); +import { getS3Url } from "@/modules/stores/aws"; -module.exports = ( - title, - content +const emailPage = ( + title: string, + content: string ) => `
@@ -29,3 +29,5 @@ module.exports = (
`; + +export default emailPage; diff --git a/src/views/loginReplacePage.js b/src/views/loginReplacePage.ts similarity index 97% rename from src/views/loginReplacePage.js rename to src/views/loginReplacePage.ts index 209e0bb0..cc20046c 100644 --- a/src/views/loginReplacePage.js +++ b/src/views/loginReplacePage.ts @@ -1,4 +1,4 @@ -module.exports = ` +const loginPage = ` @@ -44,3 +44,5 @@ module.exports = ` `; + +export default loginPage; From 76242380db2e96e86bda5881e2f2b725473d1f59 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Jan 2024 06:57:50 +0900 Subject: [PATCH 011/151] Fix: compile error --- src/middlewares/authAdmin.ts | 12 ++++++------ src/middlewares/information.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/middlewares/authAdmin.ts b/src/middlewares/authAdmin.ts index 75c67083..342906b9 100644 --- a/src/middlewares/authAdmin.ts +++ b/src/middlewares/authAdmin.ts @@ -10,25 +10,25 @@ const authAdminMiddleware = async ( ) => { try { // 로그인 여부를 확인 - if (!isLogin(req)) return res.redirect(req.origin); + if (!isLogin(req)) return res.redirect(req.origin ?? "/"); // 관리자 유무를 확인 const { id } = getLoginInfo(req); const user = await userModel.findOne({ id }); - if (!user.isAdmin) return res.redirect(req.origin); + if (!user.isAdmin) return res.redirect(req.origin ?? "/"); // 접속한 IP가 화이트리스트에 있는지 확인 const ipWhitelist = await adminIPWhitelistModel.find({}); - if (!req.clientIP) return res.redirect(req.origin); + if (!req.clientIP) return res.redirect(req.origin ?? "/"); if ( ipWhitelist.length > 0 && - ipWhitelist.map((x) => x.ip).indexOf(req.clientIP) < 0 + ipWhitelist.map((x: any) => x.ip).indexOf(req.clientIP) < 0 // TODO: Remove any ) - return res.redirect(req.origin); + return res.redirect(req.origin ?? "/"); return next(); } catch (e) { - return res.redirect(req.origin); + return res.redirect(req.origin ?? "/"); } }; diff --git a/src/middlewares/information.ts b/src/middlewares/information.ts index 3d2cd606..4d209eec 100644 --- a/src/middlewares/information.ts +++ b/src/middlewares/information.ts @@ -5,7 +5,7 @@ const informationMiddleware = ( res: Response, next: NextFunction ) => { - req.clientIP = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + req.clientIP = req.headers["x-forwarded-for"] || req.connection.remoteAddress; req.timestamp = Date.now(); next(); }; From f5ece38f7e282265efc6e96b75032e66667ae88f Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Jan 2024 22:12:39 +0900 Subject: [PATCH 012/151] Refactor: migrate middlewares/ajv to TS --- src/middlewares/ajv.js | 45 ------------------------------------------ src/middlewares/ajv.ts | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 45 deletions(-) delete mode 100644 src/middlewares/ajv.js create mode 100644 src/middlewares/ajv.ts diff --git a/src/middlewares/ajv.js b/src/middlewares/ajv.js deleted file mode 100644 index 0c8a8a3b..00000000 --- a/src/middlewares/ajv.js +++ /dev/null @@ -1,45 +0,0 @@ -const Ajv = require("ajv"); -const ajvErrors = require("ajv-errors"); -const { default: addFormats } = require("ajv-formats"); - -const ajv = new Ajv({ verbose: true, allErrors: true }); -addFormats(ajv); -ajvErrors(ajv); - -const parseAjvErrors = (errors, res) => { - const error_message = errors; - res.status(400).send(error_message); -}; - -const validate = (schema, req, res) => { - const validate = ajv.compile(schema); - if (validate(req)) { - return true; - } else { - parseAjvErrors(validate.errors[0].message, res); - } -}; - -const validateBody = (schema) => { - return (req, res, next) => { - if (validate(schema, req.body, res)) return next(); - }; -}; - -const validateQuery = (schema) => { - return (req, res, next) => { - if (validate(schema, req.query, res)) return next(); - }; -}; - -const validateParams = (schema) => { - return (req, res, next) => { - if (validate(schema, req.params, res)) return next(); - }; -}; - -module.exports = { - validateParams, - validateBody, - validateQuery, -}; diff --git a/src/middlewares/ajv.ts b/src/middlewares/ajv.ts new file mode 100644 index 00000000..359adc6e --- /dev/null +++ b/src/middlewares/ajv.ts @@ -0,0 +1,36 @@ +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import { default as addFormats } from "ajv-formats"; +import { type Request, type Response, type NextFunction } from "express"; + +const ajv = new Ajv({ verbose: true, allErrors: true }); +addFormats(ajv); +ajvErrors(ajv); + +const validate = (schema: Object, req: Request, res: Response) => { + const validate = ajv.compile(schema); + if (validate(req)) { + return true; + } else { + res.status(400).send(validate.errors?.[0].message ?? "Validation Error"); // TODO: 에러 메시지 수정 + return false; + } +}; + +export const validateBody = (schema: Object) => { + return (req: Request, res: Response, next: NextFunction) => { + if (validate(schema, req, res)) return next(); + }; +}; + +export const validateQuery = (schema: Object) => { + return (req: Request, res: Response, next: NextFunction) => { + if (validate(schema, req, res)) return next(); + }; +}; + +export const validateParams = (schema: Object) => { + return (req: Request, res: Response, next: NextFunction) => { + if (validate(schema, req, res)) return next(); + }; +}; From 5deedf9941e64538ee05f965acd0438aa16eca55 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 26 Jan 2024 22:44:48 +0900 Subject: [PATCH 013/151] Refactor: migrate modules/stores/mongo to TS --- src/middlewares/authAdmin.ts | 2 +- src/modules/stores/{mongo.js => mongo.ts} | 82 ++++++------ types/mongo.d.ts | 153 ++++++++++++++++++++++ 3 files changed, 193 insertions(+), 44 deletions(-) rename src/modules/stores/{mongo.js => mongo.ts} (72%) create mode 100644 types/mongo.d.ts diff --git a/src/middlewares/authAdmin.ts b/src/middlewares/authAdmin.ts index 342906b9..af9c7b5d 100644 --- a/src/middlewares/authAdmin.ts +++ b/src/middlewares/authAdmin.ts @@ -15,7 +15,7 @@ const authAdminMiddleware = async ( // 관리자 유무를 확인 const { id } = getLoginInfo(req); const user = await userModel.findOne({ id }); - if (!user.isAdmin) return res.redirect(req.origin ?? "/"); + if (!user?.isAdmin) return res.redirect(req.origin ?? "/"); // 접속한 IP가 화이트리스트에 있는지 확인 const ipWhitelist = await adminIPWhitelistModel.find({}); diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.ts similarity index 72% rename from src/modules/stores/mongo.js rename to src/modules/stores/mongo.ts index 7aaebb27..f69e206a 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.ts @@ -1,9 +1,8 @@ -const mongoose = require("mongoose"); -const Schema = mongoose.Schema; +import mongoose, { Schema, Model } from "mongoose"; +import logger from "@/modules/logger"; +import type { User, Participant, DeviceToken, NotificationOption, TopicSubscription, Room, Location, Chat, Report, AdminIPWhitelist, AdminLog } from "@/../types/mongo"; // TODO: Am I right..? -const logger = require("@/modules/logger"); - -const userSchema = Schema({ +const userSchema = new Schema, User>({ name: { type: String, required: true }, //실명 nickname: { type: String, required: true }, //닉네임 id: { type: String, required: true, unique: true }, //택시 서비스에서만 사용되는 id @@ -26,7 +25,7 @@ const userSchema = Schema({ account: { type: String, default: "" }, //계좌번호 정보 }); -const participantSchema = Schema({ +const participantSchema = new Schema, Participant>({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, settlementStatus: { type: String, @@ -37,7 +36,7 @@ const participantSchema = Schema({ readAt: { type: Date }, }); -const deviceTokenSchema = Schema({ +const deviceTokenSchema = new Schema, DeviceToken>({ userId: { type: Schema.Types.ObjectId, ref: "User", @@ -48,7 +47,7 @@ const deviceTokenSchema = Schema({ }); // 각 디바이스의 알림 설정 -const notificationOptionSchema = Schema({ +const notificationOptionSchema = new Schema, NotificationOption>({ deviceToken: { type: String, required: true, @@ -82,7 +81,7 @@ const notificationOptionSchema = Schema({ }, //광고성 알림 수신 여부 }); -const topicSubscriptionSchema = Schema({ +const topicSubscriptionSchema = new Schema, TopicSubscription>({ deviceToken: String, topic: String, subscribedAt: { @@ -92,7 +91,7 @@ const topicSubscriptionSchema = Schema({ }, }); -const roomSchema = Schema({ +const roomSchema = new Schema, Room>({ name: { type: String, required: true, default: "이름 없음", text: true }, from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, to: { type: Schema.Types.ObjectId, ref: "Location", required: true }, @@ -100,7 +99,7 @@ const roomSchema = Schema({ part: { type: [participantSchema], validate: [ - function (value) { + function (this: Room, value: Participant[]) { return value.length <= this.maxPartLength; }, ], @@ -110,7 +109,7 @@ const roomSchema = Schema({ maxPartLength: { type: Number, require: true, default: 4 }, }); -const locationSchema = Schema({ +const locationSchema = new Schema, Location>({ enName: { type: String, required: true }, koName: { type: String, required: true }, priority: { type: Number, default: 0 }, @@ -119,7 +118,7 @@ const locationSchema = Schema({ longitude: { type: Number }, // 이후 required: true 로 수정 필요 }); -const chatSchema = Schema({ +const chatSchema = new Schema, Chat>({ roomId: { type: Schema.Types.ObjectId, ref: "Room", required: true }, type: { type: String, @@ -142,7 +141,7 @@ const chatSchema = Schema({ }); chatSchema.index({ roomId: 1, time: -1 }); -const reportSchema = Schema({ +const reportSchema = new Schema, Report>({ creatorId: { type: Schema.Types.ObjectId, ref: "User", required: true }, // 신고한 사람 id reportedId: { type: Schema.Types.ObjectId, ref: "User", required: true }, // 신고받은 사람 id type: { @@ -155,12 +154,12 @@ const reportSchema = Schema({ roomId: { type: Schema.Types.ObjectId, ref: "Room" }, // 신고한 방 id }); -const adminIPWhitelistSchema = Schema({ +const adminIPWhitelistSchema = new Schema, AdminIPWhitelist>({ ip: { type: String, required: true }, // IP 주소 description: { type: String, default: "" }, // 설명 }); -const adminLogSchema = Schema({ +const adminLogSchema = new Schema, AdminLog>({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, // Log 취급자 User time: { type: Date, required: true }, // Log 발생 시각 ip: { type: String, required: true }, // 접속 IP 주소 @@ -184,45 +183,42 @@ database.on("error", function (err) { mongoose.disconnect(); }); -const connectDatabase = (mongoUrl) => { +export const connectDatabase = (mongoUrl: string) => { database.on("disconnected", function () { // 데이터베이스 연결이 끊어지면 5초 후 재연결을 시도합니다. logger.error("데이터베이스와 연결이 끊어졌습니다!"); setTimeout(() => { - mongoose.connect(mongoUrl, { + mongoose.connect(mongoUrl, /*{ useNewUrlParser: true, useUnifiedTopology: true, - }); + }*/); // NOTE: https://velog.io/@untiring_dev/MongoDB-MongoDB-Mongoose%EC%97%90-%EC%97%B0%EA%B2%B0 }, 5000); }); - mongoose.connect(mongoUrl, { + mongoose.connect(mongoUrl, /*{ useNewUrlParser: true, useUnifiedTopology: true, - }); + }*/); return database; }; -module.exports = { - connectDatabase, - userModel: mongoose.model("User", userSchema), - deviceTokenModel: mongoose.model("DeviceToken", deviceTokenSchema), - notificationOptionModel: mongoose.model( - "NotificationOption", - notificationOptionSchema - ), - topicSubscriptionModel: mongoose.model( - "TopicSubscription", - topicSubscriptionSchema - ), - roomModel: mongoose.model("Room", roomSchema), - locationModel: mongoose.model("Location", locationSchema), - chatModel: mongoose.model("Chat", chatSchema), - reportModel: mongoose.model("Report", reportSchema), - adminIPWhitelistModel: mongoose.model( - "AdminIPWhitelist", - adminIPWhitelistSchema - ), - adminLogModel: mongoose.model("AdminLog", adminLogSchema), -}; +export const userModel = mongoose.model("User", userSchema); +export const deviceTokenModel = mongoose.model("DeviceToken", deviceTokenSchema); +export const notificationOptionModel = mongoose.model( + "NotificationOption", + notificationOptionSchema +); +export const topicSubscriptionModel = mongoose.model( + "TopicSubscription", + topicSubscriptionSchema +); +export const roomModel = mongoose.model("Room", roomSchema); +export const locationModel = mongoose.model("Location", locationSchema); +export const chatModel = mongoose.model("Chat", chatSchema); +export const reportModel = mongoose.model("Report", reportSchema); +export const adminIPWhitelistModel = mongoose.model( + "AdminIPWhitelist", + adminIPWhitelistSchema +); +export const adminLogModel = mongoose.model("AdminLog", adminLogSchema); diff --git a/types/mongo.d.ts b/types/mongo.d.ts new file mode 100644 index 00000000..c7f63359 --- /dev/null +++ b/types/mongo.d.ts @@ -0,0 +1,153 @@ +import { Types } from "mongoose"; + +export interface User { + /** 사용자의 실명. */ + name: string; + /** 사용자의 닉네임. */ + nickname: string; + /** Taxi에서만 사용되는 사용자의 ID. */ + id: string; + /** 계정 프로필 이미지 주소. */ + profileImageUrl: string; + /** 사용자가 참여한 방 중 현재 진행 중인 방의 배열. */ + ongoingRoom?: string[]; + /** 사용자가 참여한 방 중 완료된 방의 배열. */ + doneRoom?: string[]; + withdraw?: boolean; + /** 사용자의 전화번호. 2023 가을 이벤트부터 추가됨. */ + phoneNumber?: string; + /** 계정 정지 여부. */ + ban?: boolean; + /** 계정 가입 시각. */ + joinat: Date; + /** 사용자의 Taxi 이용약관 동의 여부. */ + agreeOnTermsOfService?: boolean; + subinfo?: { + /** 사용자의 KAIST 학번. */ + kaist?: string, + sparcs?: string, + facebook?: string, + twitter?: string, + }; + /** 사용자의 이메일 주소. */ + email: string; + /** 계정의 관리자 여부. */ + isAdmin?: boolean; + /** 사용자의 계좌번호 정보. */ + account?: string; +} + +export interface Participant { + /** 방 참여자의 User ObjectID. */ + user: Types.ObjectId; + /** 방 참여자의 정산 상태. */ + settlementStatus: "not-departed" | "paid" | "send-required" | "sent"; + /** 방 참여자가 마지막으로 채팅을 읽은 시각. */ + readAt?: Date; +} + +export interface DeviceToken { + /** 디바이스 토큰 소유자의 User ObjectID. */ + userId: Types.ObjectId; + /** 소유한 디바이스 토큰의 배열. */ + deviceTokens: string[]; +} + +export interface NotificationOption { + deviceToken: string; + /** 채팅 알림 수신 여부. */ + chatting: boolean; + /** 방 알림 키워드. */ + keywords: string[]; + /** 출발 전 알림 발송 여부. */ + beforeDepart: boolean; + /** 공지성 알림 수신 여부. */ + notice: boolean; + /** 광고성 알림 수신 여부. */ + advertisement: boolean; +} + +export interface TopicSubscription { + deviceToken?: string; + topic?: string; + subscribedAt: Date; +} + +export interface Room { + /** 방의 이름. */ + name: string; + /** 방의 출발지의 Location ObjectID. */ + from: Types.ObjectId; + /** 방의 목적지의 Location ObjectID. */ + to: Types.ObjectId; + /** 방의 출발 시각. */ + time: Date; + /** 방 참여자의 배열. */ + part?: Participant[]; + /** 방의 생성 시각. */ + madeat: Date; + /** 방 참여자 중 정산을 완료한 참여자의 수. */ + settlementTotal: number; + /** 방의 최대 참여자 수. */ + maxPartLength: number; +} + +export interface Location { + enName: string; + koName: string; + priority?: number; + isValid?: boolean; + /** 위도. */ + latitude?: number; + /** 경도. */ + longitude?: number; +} + +export interface Chat { + /** 메세지가 전송된 방의 Room ObjectID. */ + roomId: Types.ObjectId; + /** 메세지의 종류. */ + type?: + | "text" + | "in" + | "out" + | "s3img" + | "payment" + | "settlement" + | "account" + | "departure" + | "arrival"; + /** 메세지의 작성자의 User ObjectID. */ + authorId?: Types.ObjectId; + content?: string; + time: Date; + isValid?: boolean; +} + +export interface Report { + /** 신고한 사용자의 ObjectID. */ + creatorId: Types.ObjectId; + /** 신고받은 사용자의 ObjectID. */ + reportedId: Types.ObjectId; + /** 신고의 종류. */ + type: "no-settlement" | "no-show" | "etc-reason"; + /** 신고의 기타 세부 사유. */ + etcDetail?: string; + /** 신고한 시각. */ + time: Date; + /** 신고한 방의 ObjectID. */ + roomId?: Types.ObjectId; +} + +export interface AdminIPWhitelist { + ip: string; + description?: string; +} + +export interface AdminLog { + user: Types.ObjectId; + time: Date; + ip: string; + target?: string; + action: "create" | "read" | "update" | "delete"; +} From ea89f43fbe5032f7cb90c8c15a225125a28df4d8 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 2 Feb 2024 08:26:54 +0900 Subject: [PATCH 014/151] Refactor: mongo.ts --- src/modules/stores/mongo.ts | 79 +++++++++++++++++++++---------------- types/mongo.d.ts | 51 +++++++++++++----------- 2 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index f69e206a..ef608f99 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -1,8 +1,8 @@ -import mongoose, { Schema, Model } from "mongoose"; +import mongoose, { model, Schema, Types } from "mongoose"; import logger from "@/modules/logger"; import type { User, Participant, DeviceToken, NotificationOption, TopicSubscription, Room, Location, Chat, Report, AdminIPWhitelist, AdminLog } from "@/../types/mongo"; // TODO: Am I right..? -const userSchema = new Schema, User>({ +const userSchema = new Schema({ name: { type: String, required: true }, //실명 nickname: { type: String, required: true }, //닉네임 id: { type: String, required: true, unique: true }, //택시 서비스에서만 사용되는 id @@ -25,7 +25,9 @@ const userSchema = new Schema, User>({ account: { type: String, default: "" }, //계좌번호 정보 }); -const participantSchema = new Schema, Participant>({ +export const userModel = model("User", userSchema); + +const participantSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, settlementStatus: { type: String, @@ -36,7 +38,7 @@ const participantSchema = new Schema, Participan readAt: { type: Date }, }); -const deviceTokenSchema = new Schema, DeviceToken>({ +const deviceTokenSchema = new Schema({ userId: { type: Schema.Types.ObjectId, ref: "User", @@ -46,8 +48,10 @@ const deviceTokenSchema = new Schema, DeviceToke deviceTokens: [{ type: String, required: true }], }); +export const deviceTokenModel = model("DeviceToken", deviceTokenSchema); + // 각 디바이스의 알림 설정 -const notificationOptionSchema = new Schema, NotificationOption>({ +const notificationOptionSchema = new Schema({ deviceToken: { type: String, required: true, @@ -81,7 +85,12 @@ const notificationOptionSchema = new Schema, TopicSubscription>({ +export const notificationOptionModel = model( + "NotificationOption", + notificationOptionSchema +); + +const topicSubscriptionSchema = new Schema({ deviceToken: String, topic: String, subscribedAt: { @@ -91,7 +100,12 @@ const topicSubscriptionSchema = new Schema, Room>({ +export const topicSubscriptionModel = model( + "TopicSubscription", + topicSubscriptionSchema +); + +const roomSchema = new Schema({ name: { type: String, required: true, default: "이름 없음", text: true }, from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, to: { type: Schema.Types.ObjectId, ref: "Location", required: true }, @@ -99,7 +113,7 @@ const roomSchema = new Schema, Room>({ part: { type: [participantSchema], validate: [ - function (this: Room, value: Participant[]) { + function (this: Room, value: Types.DocumentArray) { return value.length <= this.maxPartLength; }, ], @@ -109,16 +123,20 @@ const roomSchema = new Schema, Room>({ maxPartLength: { type: Number, require: true, default: 4 }, }); -const locationSchema = new Schema, Location>({ +export const roomModel = model("Room", roomSchema); + +const locationSchema = new Schema({ enName: { type: String, required: true }, koName: { type: String, required: true }, priority: { type: Number, default: 0 }, isValid: { type: Boolean, default: true }, - latitude: { type: Number }, // 이후 required: true 로 수정 필요 - longitude: { type: Number }, // 이후 required: true 로 수정 필요 + latitude: { type: Number, required: true }, + longitude: { type: Number, required: true }, }); -const chatSchema = new Schema, Chat>({ +export const locationModel = model("Location", locationSchema); + +const chatSchema = new Schema({ roomId: { type: Schema.Types.ObjectId, ref: "Room", required: true }, type: { type: String, @@ -141,7 +159,9 @@ const chatSchema = new Schema, Chat>({ }); chatSchema.index({ roomId: 1, time: -1 }); -const reportSchema = new Schema, Report>({ +export const chatModel = model("Chat", chatSchema); + +const reportSchema = new Schema({ creatorId: { type: Schema.Types.ObjectId, ref: "User", required: true }, // 신고한 사람 id reportedId: { type: Schema.Types.ObjectId, ref: "User", required: true }, // 신고받은 사람 id type: { @@ -154,12 +174,19 @@ const reportSchema = new Schema, Report>({ roomId: { type: Schema.Types.ObjectId, ref: "Room" }, // 신고한 방 id }); -const adminIPWhitelistSchema = new Schema, AdminIPWhitelist>({ +export const reportModel = model("Report", reportSchema); + +const adminIPWhitelistSchema = new Schema({ ip: { type: String, required: true }, // IP 주소 description: { type: String, default: "" }, // 설명 }); -const adminLogSchema = new Schema, AdminLog>({ +export const adminIPWhitelistModel = model( + "AdminIPWhitelist", + adminIPWhitelistSchema +); + +const adminLogSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, // Log 취급자 User time: { type: Date, required: true }, // Log 발생 시각 ip: { type: String, required: true }, // 접속 IP 주소 @@ -171,6 +198,8 @@ const adminLogSchema = new Schema, AdminLog>({ }, // 수행 업무 }); +export const adminLogModel = model("AdminLog", adminLogSchema); + mongoose.set("strictQuery", true); const database = mongoose.connection; @@ -202,23 +231,3 @@ export const connectDatabase = (mongoUrl: string) => { return database; }; - -export const userModel = mongoose.model("User", userSchema); -export const deviceTokenModel = mongoose.model("DeviceToken", deviceTokenSchema); -export const notificationOptionModel = mongoose.model( - "NotificationOption", - notificationOptionSchema -); -export const topicSubscriptionModel = mongoose.model( - "TopicSubscription", - topicSubscriptionSchema -); -export const roomModel = mongoose.model("Room", roomSchema); -export const locationModel = mongoose.model("Location", locationSchema); -export const chatModel = mongoose.model("Chat", chatSchema); -export const reportModel = mongoose.model("Report", reportSchema); -export const adminIPWhitelistModel = mongoose.model( - "AdminIPWhitelist", - adminIPWhitelistSchema -); -export const adminLogModel = mongoose.model("AdminLog", adminLogSchema); diff --git a/types/mongo.d.ts b/types/mongo.d.ts index c7f63359..5010c22e 100644 --- a/types/mongo.d.ts +++ b/types/mongo.d.ts @@ -10,31 +10,31 @@ export interface User { /** 계정 프로필 이미지 주소. */ profileImageUrl: string; /** 사용자가 참여한 방 중 현재 진행 중인 방의 배열. */ - ongoingRoom?: string[]; + ongoingRoom?: Types.Array; /** 사용자가 참여한 방 중 완료된 방의 배열. */ - doneRoom?: string[]; - withdraw?: boolean; + doneRoom?: Types.Array; + withdraw: boolean; /** 사용자의 전화번호. 2023 가을 이벤트부터 추가됨. */ phoneNumber?: string; /** 계정 정지 여부. */ - ban?: boolean; + ban: boolean; /** 계정 가입 시각. */ joinat: Date; /** 사용자의 Taxi 이용약관 동의 여부. */ - agreeOnTermsOfService?: boolean; + agreeOnTermsOfService: boolean; subinfo?: { /** 사용자의 KAIST 학번. */ - kaist?: string, - sparcs?: string, - facebook?: string, - twitter?: string, + kaist: string, + sparcs: string, + facebook: string, + twitter: string, }; /** 사용자의 이메일 주소. */ email: string; /** 계정의 관리자 여부. */ - isAdmin?: boolean; + isAdmin: boolean; /** 사용자의 계좌번호 정보. */ - account?: string; + account: string; } export interface Participant { @@ -50,7 +50,7 @@ export interface DeviceToken { /** 디바이스 토큰 소유자의 User ObjectID. */ userId: Types.ObjectId; /** 소유한 디바이스 토큰의 배열. */ - deviceTokens: string[]; + deviceTokens: Types.Array; } export interface NotificationOption { @@ -58,7 +58,7 @@ export interface NotificationOption { /** 채팅 알림 수신 여부. */ chatting: boolean; /** 방 알림 키워드. */ - keywords: string[]; + keywords: Types.Array; /** 출발 전 알림 발송 여부. */ beforeDepart: boolean; /** 공지성 알림 수신 여부. */ @@ -83,7 +83,7 @@ export interface Room { /** 방의 출발 시각. */ time: Date; /** 방 참여자의 배열. */ - part?: Participant[]; + part?: Types.DocumentArray; /** 방의 생성 시각. */ madeat: Date; /** 방 참여자 중 정산을 완료한 참여자의 수. */ @@ -95,12 +95,12 @@ export interface Room { export interface Location { enName: string; koName: string; - priority?: number; - isValid?: boolean; + priority: number; + isValid: boolean; /** 위도. */ - latitude?: number; + latitude: number; /** 경도. */ - longitude?: number; + longitude: number; } export interface Chat { @@ -119,9 +119,9 @@ export interface Chat { | "arrival"; /** 메세지의 작성자의 User ObjectID. */ authorId?: Types.ObjectId; - content?: string; + content: string; time: Date; - isValid?: boolean; + isValid: boolean; } export interface Report { @@ -132,7 +132,7 @@ export interface Report { /** 신고의 종류. */ type: "no-settlement" | "no-show" | "etc-reason"; /** 신고의 기타 세부 사유. */ - etcDetail?: string; + etcDetail: string; /** 신고한 시각. */ time: Date; /** 신고한 방의 ObjectID. */ @@ -141,13 +141,18 @@ export interface Report { export interface AdminIPWhitelist { ip: string; - description?: string; + description: string; } export interface AdminLog { + /** 로그 발생자의 User ObjectID. */ user: Types.ObjectId; + /** 로그의 발생 시각. */ time: Date; + /** 로그의 발생 IP 주소. */ ip: string; - target?: string; + /** 취급한 대상. */ + target: string; + /** 수행한 업무. */ action: "create" | "read" | "update" | "delete"; } From db8ce7ed30f60fe40a24d1dddd8e5f8a62ec9f73 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 2 Feb 2024 08:34:40 +0900 Subject: [PATCH 015/151] Refactor: migrate modules/stores/aws to TS --- src/modules/stores/{aws.js => aws.ts} | 44 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 12 deletions(-) rename src/modules/stores/{aws.js => aws.ts} (68%) diff --git a/src/modules/stores/aws.js b/src/modules/stores/aws.ts similarity index 68% rename from src/modules/stores/aws.js rename to src/modules/stores/aws.ts index 2947410c..f46c9628 100644 --- a/src/modules/stores/aws.js +++ b/src/modules/stores/aws.ts @@ -1,8 +1,8 @@ -const { aws: awsEnv } = require("@/loadenv"); +import AWS from "aws-sdk"; +import { aws as awsEnv } from "@/loadenv"; +import logger from "@/modules/logger"; +import { type Report } from "@/../types/mongo"; // TODO: 이게맞나 -const logger = require("@/modules/logger"); -// Load the AWS-SDK and s3 -const AWS = require("aws-sdk"); AWS.config.update({ region: "ap-northeast-2", signatureVersion: "v4", @@ -12,7 +12,10 @@ const s3 = new AWS.S3({ apiVersion: "2006-03-01" }); const ses = new AWS.SES({ apiVersion: "2010-12-01" }); // function to list Object -module.exports.getList = (directoryPath, cb) => { +export const getList = ( + directoryPath: string, + cb: (err: AWS.AWSError, data: AWS.S3.ListObjectsOutput) => void +) => { s3.listObjects( { Bucket: awsEnv.s3BucketName, @@ -25,7 +28,10 @@ module.exports.getList = (directoryPath, cb) => { }; // function to generate signed-url for upload(PUT) -module.exports.getUploadPUrlPut = (filePath, contentType = "image/png") => { +export const getUploadPUrlPut = ( + filePath: string, + contentType: string = "image/png" +) => { const presignedUrl = s3.getSignedUrl("putObject", { Bucket: awsEnv.s3BucketName, Key: filePath, @@ -36,7 +42,11 @@ module.exports.getUploadPUrlPut = (filePath, contentType = "image/png") => { }; // function to generate signed-url for upload(POST) -module.exports.getUploadPUrlPost = (filePath, contentType, cb) => { +export const getUploadPUrlPost = ( + filePath: string, + contentType: string, + cb: (err: Error, data: AWS.S3.PresignedPost) => void +) => { s3.createPresignedPost( { Bucket: awsEnv.s3BucketName, @@ -54,7 +64,10 @@ module.exports.getUploadPUrlPost = (filePath, contentType, cb) => { }; // function to delete object -module.exports.deleteObject = (filePath, cb) => { +export const deleteObject = ( + filePath: string, + cb: (err: AWS.AWSError, data: AWS.S3.DeleteObjectOutput) => void +) => { s3.deleteObject( { Bucket: awsEnv.s3BucketName, @@ -67,7 +80,10 @@ module.exports.deleteObject = (filePath, cb) => { }; // function to check exist of Object -module.exports.foundObject = (filePath, cb) => { +export const foundObject = ( + filePath: string, + cb: (err: AWS.AWSError, data: AWS.S3.HeadObjectOutput) => void +) => { s3.headObject( { Bucket: awsEnv.s3BucketName, @@ -80,11 +96,15 @@ module.exports.foundObject = (filePath, cb) => { }; // function to return full URL of the object -module.exports.getS3Url = (filePath) => { +export const getS3Url = (filePath: string) => { return `${awsEnv.s3Url}${filePath}`; }; -module.exports.sendReportEmail = (reportedEmail, report, html) => { +export const sendReportEmail = ( + reportedEmail: string, + report: Report, + html: string +) => { const reportTypeMap = { "no-settlement": "정산을 하지 않음", "no-show": "택시에 동승하지 않음", @@ -111,7 +131,7 @@ module.exports.sendReportEmail = (reportedEmail, report, html) => { Source: "taxi.sparcs@gmail.com", }; - ses.sendEmail(params, (err, data) => { + ses.sendEmail(params, (err) => { if (err) { logger.error("Fail to send email", err); } else { From b6cdd88e8818b85258a46f6bbfe8389e26cd3840 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 2 Feb 2024 11:42:37 +0900 Subject: [PATCH 016/151] Refactor: migrate modules/stores/sessionStore to TS --- package.json | 2 +- pnpm-lock.yaml | 14 ++++++++----- .../{sessionStore.js => sessionStore.ts} | 20 +++++++------------ 3 files changed, 17 insertions(+), 19 deletions(-) rename src/modules/stores/{sessionStore.js => sessionStore.ts} (60%) diff --git a/package.json b/package.json index e9d3ff5e..feab2b26 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "axios": "^0.27.2", "ci": "^2.2.0", "connect-mongo": "^4.6.0", - "connect-redis": "^6.1.3", + "connect-redis": "^7.1.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "cross-env": "^7.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 469bd3d7..bf573a51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ dependencies: specifier: ^4.6.0 version: 4.6.0(express-session@1.17.3)(mongodb@4.17.1) connect-redis: - specifier: ^6.1.3 - version: 6.1.3 + specifier: ^7.1.1 + version: 7.1.1(express-session@1.17.3) cookie-parser: specifier: ^1.4.5 version: 1.4.6 @@ -4829,9 +4829,13 @@ packages: - supports-color dev: false - /connect-redis@6.1.3: - resolution: {integrity: sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw==} - engines: {node: '>=12'} + /connect-redis@7.1.1(express-session@1.17.3): + resolution: {integrity: sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==} + engines: {node: '>=16'} + peerDependencies: + express-session: '>=1' + dependencies: + express-session: 1.17.3 dev: false /content-disposition@0.5.4: diff --git a/src/modules/stores/sessionStore.js b/src/modules/stores/sessionStore.ts similarity index 60% rename from src/modules/stores/sessionStore.js rename to src/modules/stores/sessionStore.ts index 3d247d54..11647e00 100644 --- a/src/modules/stores/sessionStore.js +++ b/src/modules/stores/sessionStore.ts @@ -1,20 +1,14 @@ -const expressSession = require("express-session"); -const redis = require("redis"); -const MongoStore = require("connect-mongo"); -const RedisStore = require("connect-redis")(expressSession); -const { - redis: redisUrl, - mongo: mongoUrl, - session: sessionConfig, -} = require("@/loadenv"); -const logger = require("@/modules/logger"); +import MongoStore from "connect-mongo"; +import RedisStore from "connect-redis" +import redis from "redis"; +import { redis as redisUrl, mongo as mongoUrl, session as sessionConfig } from "@/loadenv"; +import logger from "@/modules/logger"; -const getSessionStore = (redisUrl) => { +const getSessionStore = () => { // 환경변수 REDIS_PATH 유무에 따라 session 저장 방식이 변경됩니다. if (redisUrl) { const client = redis.createClient({ url: redisUrl, - legacyMode: true, }); // redis client 연결 성공 시 로그를 출력합니다. @@ -34,4 +28,4 @@ const getSessionStore = (redisUrl) => { } }; -module.exports = getSessionStore(redisUrl); +export default getSessionStore(); From 94daf703427cd1aa2044dbba2620a0523e3e5f71 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 3 Feb 2024 04:48:47 +0900 Subject: [PATCH 017/151] Refactor: migrate some files in modules directory to TS --- .../{modifyProfile.js => modifyProfile.ts} | 21 +++++++------------ src/modules/{patterns.js => patterns.ts} | 2 +- ...ckNotification.js => slackNotification.ts} | 13 ++++++------ types/mongo.d.ts | 4 +++- 4 files changed, 18 insertions(+), 22 deletions(-) rename src/modules/{modifyProfile.js => modifyProfile.ts} (83%) rename src/modules/{patterns.js => patterns.ts} (97%) rename src/modules/{slackNotification.js => slackNotification.ts} (59%) diff --git a/src/modules/modifyProfile.js b/src/modules/modifyProfile.ts similarity index 83% rename from src/modules/modifyProfile.js rename to src/modules/modifyProfile.ts index e8702f98..a440e484 100755 --- a/src/modules/modifyProfile.js +++ b/src/modules/modifyProfile.ts @@ -1,5 +1,5 @@ -const crypto = require("crypto"); -const aws = require("./stores/aws"); +import crypto from "crypto"; +import { getS3Url } from "@/modules/stores/aws"; const nouns = [ "재료역학", @@ -64,7 +64,7 @@ const defaultProfile = [ // 닉네임 규칙에 따라 새 유저의 닉네임을 생성해 반환합니다. // Ara의 닉네임 생성 규칙을 참고하였습니다. -const generateNickname = (id) => { +export const generateNickname = (id: string) => { const nounIdx = crypto.randomInt(nouns.length); const adjectiveIdx = crypto.randomInt(adjectives.length); const noun = nouns[nounIdx]; @@ -80,26 +80,19 @@ const generateNickname = (id) => { }; // 기존 프로필 사진의 URI 중 하나를 무작위로 선택해 반환합니다. -const generateProfileImageUrl = () => { +export const generateProfileImageUrl = () => { const ridx = crypto.randomInt(defaultProfile.length); - return aws.getS3Url(`/profile-img/default/${defaultProfile[ridx]}`); + return getS3Url(`/profile-img/default/${defaultProfile[ridx]}`); }; // 사용자의 이름과 성을 받아, 한글인지 영어인지에 따라 전체 이름을 반환합니다. -const getFullUsername = (firstName, lastName) => { +export const getFullUsername = (firstName: string, lastName: string) => { const koPattern = new RegExp("[가-힣]+"); if (koPattern.test(firstName) && koPattern.test(lastName)) return `${lastName}${firstName}`; else return `${firstName} ${lastName}`; }; -const replaceSpaceInNickname = (nickname) => { +export const replaceSpaceInNickname = (nickname: string) => { return nickname.replace(/\s+/g, " "); }; - -module.exports = { - generateNickname, - generateProfileImageUrl, - getFullUsername, - replaceSpaceInNickname, -}; diff --git a/src/modules/patterns.js b/src/modules/patterns.ts similarity index 97% rename from src/modules/patterns.js rename to src/modules/patterns.ts index 7111ec1a..8e9adc56 100644 --- a/src/modules/patterns.js +++ b/src/modules/patterns.ts @@ -1,4 +1,4 @@ -module.exports = { +export default { room: { name: RegExp( "^[A-Za-z0-9가-힣ㄱ-ㅎㅏ-ㅣ,.?! _~/#'\\\\@=\"\\-\\^()+*<>{}[\\]]{1,50}$" // ,.?/#'\@="-^()+*<>{}[] 허용 diff --git a/src/modules/slackNotification.js b/src/modules/slackNotification.ts similarity index 59% rename from src/modules/slackNotification.js rename to src/modules/slackNotification.ts index 4ac3d378..e03386a1 100644 --- a/src/modules/slackNotification.js +++ b/src/modules/slackNotification.ts @@ -1,8 +1,9 @@ -const { slackWebhookUrl: slackUrl } = require("@/loadenv"); -const axios = require("axios"); -const logger = require("./logger"); +import axios from "axios"; +import { slackWebhookUrl as slackUrl } from "@/loadenv"; +import logger from "@/modules/logger"; +import { type Report } from "@/../types/mongo"; -module.exports.notifyToReportChannel = (reportUser, report) => { +export const notifyToReportChannel = (reportUser: string, report: Report) => { if (!slackUrl.report) return; const data = { @@ -15,11 +16,11 @@ module.exports.notifyToReportChannel = (reportUser, report) => { 기타: ${report.etcDetail} `, }; - const config = { "Content-Type": "application/json" }; + const config = { headers: { "Content-Type": "application/json" } }; axios .post(slackUrl.report, data, config) - .then((res) => { + .then(() => { logger.info("Slack webhook sent successfully"); }) .catch((err) => { diff --git a/types/mongo.d.ts b/types/mongo.d.ts index 5010c22e..0929eccc 100644 --- a/types/mongo.d.ts +++ b/types/mongo.d.ts @@ -144,6 +144,8 @@ export interface AdminIPWhitelist { description: string; } +export type AdminLogAction = "create" | "read" | "update" | "delete"; + export interface AdminLog { /** 로그 발생자의 User ObjectID. */ user: Types.ObjectId; @@ -154,5 +156,5 @@ export interface AdminLog { /** 취급한 대상. */ target: string; /** 수행한 업무. */ - action: "create" | "read" | "update" | "delete"; + action: AdminLogAction; } From 2a7c55f538a907d277a3520e53742a873f4f1777 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 3 Feb 2024 09:27:51 +0900 Subject: [PATCH 018/151] Refactor: move types directory into src directory --- src/modules/slackNotification.ts | 2 +- src/modules/stores/aws.ts | 2 +- src/modules/stores/mongo.ts | 2 +- {types => src/types}/index.d.ts | 0 {types => src/types}/mongo.d.ts | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename {types => src/types}/index.d.ts (100%) rename {types => src/types}/mongo.d.ts (100%) diff --git a/src/modules/slackNotification.ts b/src/modules/slackNotification.ts index e03386a1..49a5d44e 100644 --- a/src/modules/slackNotification.ts +++ b/src/modules/slackNotification.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { slackWebhookUrl as slackUrl } from "@/loadenv"; import logger from "@/modules/logger"; -import { type Report } from "@/../types/mongo"; +import { type Report } from "@/types/mongo"; export const notifyToReportChannel = (reportUser: string, report: Report) => { if (!slackUrl.report) return; diff --git a/src/modules/stores/aws.ts b/src/modules/stores/aws.ts index f46c9628..67dd9f10 100644 --- a/src/modules/stores/aws.ts +++ b/src/modules/stores/aws.ts @@ -1,7 +1,7 @@ import AWS from "aws-sdk"; import { aws as awsEnv } from "@/loadenv"; import logger from "@/modules/logger"; -import { type Report } from "@/../types/mongo"; // TODO: 이게맞나 +import { type Report } from "@/types/mongo"; AWS.config.update({ region: "ap-northeast-2", diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index ef608f99..bc248bce 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -1,6 +1,6 @@ import mongoose, { model, Schema, Types } from "mongoose"; import logger from "@/modules/logger"; -import type { User, Participant, DeviceToken, NotificationOption, TopicSubscription, Room, Location, Chat, Report, AdminIPWhitelist, AdminLog } from "@/../types/mongo"; // TODO: Am I right..? +import type { User, Participant, DeviceToken, NotificationOption, TopicSubscription, Room, Location, Chat, Report, AdminIPWhitelist, AdminLog } from "@/types/mongo"; const userSchema = new Schema({ name: { type: String, required: true }, //실명 diff --git a/types/index.d.ts b/src/types/index.d.ts similarity index 100% rename from types/index.d.ts rename to src/types/index.d.ts diff --git a/types/mongo.d.ts b/src/types/mongo.d.ts similarity index 100% rename from types/mongo.d.ts rename to src/types/mongo.d.ts From 3a2ab6533482caf03225cb997048ac6e4c2a5956 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 3 Feb 2024 09:29:22 +0900 Subject: [PATCH 019/151] Refactor: update tsconfig.json --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 0e7f68ff..0f059817 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "@/*": ["./*"] } }, - "include": ["src", "types"], + "include": ["src"], "exclude": ["dist", "node_modules"] } \ No newline at end of file From 7795c73fa5545d7f4eefc226596bad70743a078b Mon Sep 17 00:00:00 2001 From: static Date: Sat, 3 Feb 2024 10:38:42 +0900 Subject: [PATCH 020/151] Refactor: migrate modules/fcm to TS --- src/modules/{fcm.js => fcm.ts} | 53 +++++++++++++--------------------- src/types/mongo.d.ts | 26 ++++++++++------- 2 files changed, 35 insertions(+), 44 deletions(-) rename src/modules/{fcm.js => fcm.ts} (88%) diff --git a/src/modules/fcm.js b/src/modules/fcm.ts similarity index 88% rename from src/modules/fcm.js rename to src/modules/fcm.ts index c9ab01d6..44074d7f 100644 --- a/src/modules/fcm.js +++ b/src/modules/fcm.ts @@ -1,17 +1,18 @@ -const firebaseAdmin = require("firebase-admin"); -const { getMessaging } = require("firebase-admin/messaging"); -const { +import firebaseAdmin from "firebase-admin"; +import { type SendResponse, getMessaging } from "firebase-admin/messaging"; +import { googleApplicationCredentials } from "@/loadenv"; +import logger from "@/modules/logger"; +import { deviceTokenModel, notificationOptionModel, topicSubscriptionModel, -} = require("@/modules/stores/mongo"); -const logger = require("./logger"); -const { googleApplicationCredentials } = require("@/loadenv"); +} from "@/modules/stores/mongo"; +import { type ChatType } from "@/types/mongo"; /** * credential을 등록합니다. */ -const initializeApp = () => { +export const initializeApp = () => { if (googleApplicationCredentials) { firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.cert(googleApplicationCredentials), @@ -29,7 +30,7 @@ const initializeApp = () => { * @param {string} deviceToken - 등록하려는 FCM device token입니다. * @return {Promise>} 변경된 사용자의 deviceToken의 목록 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ -const registerDeviceToken = async (userId, deviceToken) => { +export const registerDeviceToken = async (userId: string, deviceToken: string): Promise => { try { // 디바이스 토큰을 다른 사용자가 사용하고 있는지 확인 및 삭제합니다. await deviceTokenModel.updateMany( @@ -61,14 +62,12 @@ const registerDeviceToken = async (userId, deviceToken) => { } }; -// TODO: remove userId /** * 사용자의 ObjectId와 FCM device token이 주어졌을 때, 해당 사용자의 해당 deviceToken을 DB에서 삭제합니다. - * @param {string} userId - 사용자의 ObjectId입니다. * @param {string} deviceToken - 삭제하려는 FCM device token입니다. * @return {Promise} 해당 deviceToken을 가진 모든 사용자로부터 해당 deviceToken을 삭제하는 데 성공하면 true, 하나 이상의 사용자에게서 해당 deviceToken을 삭제하는 데 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. */ -const unregisterDeviceToken = async (deviceToken) => { +export const unregisterDeviceToken = async (deviceToken: string) => { try { // 디바이스 토큰을 DB에서 삭제합니다. const { matchedCount, modifiedCount } = await deviceTokenModel.updateMany( @@ -97,13 +96,13 @@ const unregisterDeviceToken = async (deviceToken) => { * @param {Array} fcmResponses - 등록하려는 FCM device token입니다. * @return {Promise>} 각각의 토큰들의 삭제 성공 여부가 저장된 Array를 반환합니다. 해당 토큰을 DB에서 삭제하는 데 성공했으면 true, 아니면 false가 포함됩니다. */ -const removeExpiredTokens = async (deviceTokens, fcmResponses) => { +const removeExpiredTokens = async (deviceTokens: string[], fcmResponses: SendResponse[]) => { const removalResults = await Promise.all( deviceTokens.map(async (deviceToken, index) => { try { // FCM device token이 유효하지 않아 메시지 전송에 실패한 경우, 해당 device token을 DB에서 삭제합니다. if ( - fcmResponses[index].error.code === + fcmResponses[index].error?.code === "messaging/registration-token-not-registered" ) { await unregisterDeviceToken(deviceToken); @@ -129,7 +128,7 @@ const removeExpiredTokens = async (deviceTokens, fcmResponses) => { * @param {string} deviceToken - 사용 가능 여부를 확인하려고 하는 FCM device token입니다. * @return {Promise} 해당 디바이스에 알림을 보낸다는 요청을 FCM 서버에 성공적으로 보냈으면 true, 아니면 false를 반환합니다. */ -const validateDeviceToken = async (deviceToken) => { +export const validateDeviceToken = async (deviceToken: string) => { try { const message = { token: deviceToken, @@ -152,7 +151,7 @@ const validateDeviceToken = async (deviceToken) => { * @param {Boolean?} notificationOptions.chatting - true 또는 false로 주어진 경우, 채팅 알림 설정이 각각 true 또는 false로 설정된 사용자들의 deviceToken만 반환합니다. * @return {Promise>} deviceToken의 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ -const getTokensOfUsers = async (userIds, notificationOptions = {}) => { +export const getTokensOfUsers = async (userIds: string[], notificationOptions: Object = {}) => { const deviceTokensOfUsers = ( await Promise.all( userIds.map( @@ -180,14 +179,14 @@ const getTokensOfUsers = async (userIds, notificationOptions = {}) => { * 주어진 token들에 메시지 알림을 전송합니다. * TODO: 알림 전송 실패한 토큰 삭제하기 * @param {Array} tokens - 메시지 알림을 받을 기기의 deviceToken들로 구성된 Array입니다. - * @param {string} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. + * @param {ChatType} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. * @param {string} title - 보낼 메시지의 제목입니다. * @param {string} body - 보낼 메시지의 본문입니다. * @param {string?} icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. * @return {Promise} 메시지 알림 전송에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { +export const sendMessageByTokens = async (tokens: string[], type: ChatType, title: string, body: string, icon?: string, link?: string) => { if (tokens.length === 0) return -1; try { const message = { @@ -221,14 +220,14 @@ const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { /** * 주어진 topic을 구독하고 있는 모든 기기에 메시지 알림을 전송합니다. * @param {string} topic - 메시지 알림을 보낼 기기들이 구독하고 있는 topic입니다. - * @param {string} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. + * @param {ChatType} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. * @param {string} title - 보낼 메시지의 제목입니다. * @param {string} body - 보낼 메시지의 본문입니다. * @param {string?} icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. * @return {Promise} 메시지 알림 전송에 성공했으면 true, 아니면 false를 반환합니다. */ -const sendMessageByTopic = async (topic, type, title, body, icon, link) => { +export const sendMessageByTopic = async (topic: string, type: ChatType, title: string, body: string, icon?: string, link?: string) => { try { const message = { topic, @@ -255,7 +254,7 @@ const sendMessageByTopic = async (topic, type, title, body, icon, link) => { * @param {string} topic - 구독할 topic입니다. * @return {Promise} 토픽 구독에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -const subscribeUserToTopic = async (userId, topic) => { +export const subscribeUserToTopic = async (userId: string, topic: string) => { try { const deviceToken = await deviceTokenModel.findOne({ userId, @@ -306,7 +305,7 @@ const subscribeUserToTopic = async (userId, topic) => { * @param {string} topic - 구독을 해제할 topic입니다. * @return {Promise} 토픽 구독 해제에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -const unsubscribeUserFromTopic = async (userId, topic) => { +export const unsubscribeUserFromTopic = async (userId: string, topic: string) => { try { const deviceToken = await deviceTokenModel.findOne({ userId, @@ -340,15 +339,3 @@ const unsubscribeUserFromTopic = async (userId, topic) => { return -1; } }; - -module.exports = { - initializeApp, - registerDeviceToken, - unregisterDeviceToken, - validateDeviceToken, - getTokensOfUsers, - sendMessageByTokens, - sendMessageByTopic, - subscribeUserToTopic, - unsubscribeUserFromTopic, -}; diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index 0929eccc..810755ce 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -37,11 +37,13 @@ export interface User { account: string; } +export type SettlementStatus = "not-departed" | "paid" | "send-required" | "sent"; + export interface Participant { /** 방 참여자의 User ObjectID. */ user: Types.ObjectId; /** 방 참여자의 정산 상태. */ - settlementStatus: "not-departed" | "paid" | "send-required" | "sent"; + settlementStatus: SettlementStatus; /** 방 참여자가 마지막으로 채팅을 읽은 시각. */ readAt?: Date; } @@ -103,20 +105,22 @@ export interface Location { longitude: number; } +export type ChatType = + | "text" + | "in" + | "out" + | "s3img" + | "payment" + | "settlement" + | "account" + | "departure" + | "arrival"; + export interface Chat { /** 메세지가 전송된 방의 Room ObjectID. */ roomId: Types.ObjectId; /** 메세지의 종류. */ - type?: - | "text" - | "in" - | "out" - | "s3img" - | "payment" - | "settlement" - | "account" - | "departure" - | "arrival"; + type?: ChatType; /** 메세지의 작성자의 User ObjectID. */ authorId?: Types.ObjectId; content: string; From 8d58a791ae0ae55edbc33c47734c3308d70ee1cf Mon Sep 17 00:00:00 2001 From: static Date: Sat, 3 Feb 2024 11:19:19 +0900 Subject: [PATCH 021/151] Refactor: migrate populate directory to TS --- src/modules/populates/chats.js | 10 ---- src/modules/populates/chats.ts | 12 +++++ src/modules/populates/reports.js | 10 ---- src/modules/populates/reports.ts | 12 +++++ src/modules/populates/rooms.js | 73 ------------------------- src/modules/populates/rooms.ts | 93 ++++++++++++++++++++++++++++++++ src/types/mongo.d.ts | 24 ++++----- 7 files changed, 129 insertions(+), 105 deletions(-) delete mode 100644 src/modules/populates/chats.js create mode 100644 src/modules/populates/chats.ts delete mode 100644 src/modules/populates/reports.js create mode 100644 src/modules/populates/reports.ts delete mode 100644 src/modules/populates/rooms.js create mode 100644 src/modules/populates/rooms.ts diff --git a/src/modules/populates/chats.js b/src/modules/populates/chats.js deleted file mode 100644 index 2e18ccb6..00000000 --- a/src/modules/populates/chats.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @constant {{path: string, select: string}[]} - * 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다. - */ -const chatPopulateOption = [ - { path: "authorId", select: "_id nickname profileImageUrl" }, -]; - -module.exports = { - chatPopulateOption, -}; diff --git a/src/modules/populates/chats.ts b/src/modules/populates/chats.ts new file mode 100644 index 00000000..fa92f2d6 --- /dev/null +++ b/src/modules/populates/chats.ts @@ -0,0 +1,12 @@ +import { type User, type Chat } from "@/types/mongo"; + +/** @constant {{path: string, select: string}[]} + * 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다. + */ +export const chatPopulateOption = [ + { path: "authorId", select: "_id nickname profileImageUrl" }, +]; + +export interface PopulatedChat extends Omit { + authorId?: Pick; +}; diff --git a/src/modules/populates/reports.js b/src/modules/populates/reports.js deleted file mode 100644 index 9f5b3fa4..00000000 --- a/src/modules/populates/reports.js +++ /dev/null @@ -1,10 +0,0 @@ -const reportPopulateOption = [ - { - path: "reportedId", - select: "_id id name nickname profileImageUrl", - }, -]; - -module.exports = { - reportPopulateOption, -}; diff --git a/src/modules/populates/reports.ts b/src/modules/populates/reports.ts new file mode 100644 index 00000000..439311a7 --- /dev/null +++ b/src/modules/populates/reports.ts @@ -0,0 +1,12 @@ +import { type User, type Report } from "@/types/mongo"; + +export const reportPopulateOption = [ + { + path: "reportedId", + select: "_id id name nickname profileImageUrl", + }, +]; + +export interface PopulatedReport extends Omit { + reportedId: Pick; +}; diff --git a/src/modules/populates/rooms.js b/src/modules/populates/rooms.js deleted file mode 100644 index 7243d648..00000000 --- a/src/modules/populates/rooms.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * 쿼리를 통해 얻은 Room Document를 populate할 설정값을 정의합니다. - * @constant {{path: string, select: string, populate?: {path: string, select: string}}[]} - */ -const roomPopulateOption = [ - { path: "from", select: "_id koName enName" }, - { path: "to", select: "_id koName enName" }, - { - path: "part", - select: "-_id user settlementStatus readAt", - populate: { path: "user", select: "_id id name nickname profileImageUrl" }, - }, -]; - -/** - * Room Object가 주어졌을 때 room의 part array의 각 요소를 API 명세에서와 같이 {userId: String, ... , isSettlement: String}으로 가공합니다. - * 또한, 방이 현재 출발했는지 유무인 isDeparted 속성을 추가합니다. - * @param {Object} roomObject - 정산 정보를 가공할 room Object로, Mongoose Document가 아닌 순수 Javascript Object여야 합니다. - * @param {Object} options - 추가 파라미터로, 기본값은 {}입니다. - * @param {Boolean} options.includeSettlement - 반환 결과에 정산 정보를 포함할 지 여부로, 기본값은 true입니다. - * @param {Date} options.timestamp - 방의 출발 여부(isDeparted)를 판단하는 기준이 되는 시각입니다. - * @param {Boolean} options.isOver - 방의 완료 여부(isOver)로, 기본값은 false입니다. includeSettlement가 false인 경우 roomDocument의 isOver 속성은 undefined로 설정됩니다. - * @return {Object} 정산 여부가 위와 같이 가공되고 isDeparted 속성이 추가된 Room Object가 반환됩니다. - */ -const formatSettlement = ( - roomObject, - { includeSettlement = true, isOver = false, timestamp = Date.now() } = {} -) => { - roomObject.part = roomObject.part.map((participantSubDocument) => { - const { _id, name, nickname, profileImageUrl } = - participantSubDocument.user; - const { settlementStatus, readAt } = participantSubDocument; - return { - _id, - name, - nickname, - profileImageUrl, - isSettlement: includeSettlement ? settlementStatus : undefined, - readAt: readAt ?? roomObject.madeat, - }; - }); - roomObject.settlementTotal = includeSettlement - ? roomObject.settlementTotal - : undefined; - roomObject.isOver = includeSettlement ? isOver : undefined; - roomObject.isDeparted = new Date(roomObject.time) < new Date(timestamp); - return roomObject; -}; - -/** - * formatSettlement 함수를 사용하여 변환한 Room Object와 사용자의 id(userId)가 주어졌을 때, 해당 사용자의 정산 상태를 반환합니다. - * @param {Object} roomObject - formatSettlement 함수를 사용하여 변환한 Room Object입니다. - * @param {String} userId - 방 완료 상태를 확인하려는 사용자의 id(user.id)입니다. - * @return {Boolean | undefined} 사용자의 해당 방에 대한 완료 여부(true | false)를 반환합니다. 사용자가 참여중인 방이 아닐 경우 undefined를 반환합니다. - **/ -const getIsOver = (roomObject, userId) => { - // room document의 part subdoocument에서 사용자 id와 일치하는 정산 정보를 찾습니다. - const participantSubDocuments = roomObject.part.filter((part) => { - return part.user.id === userId; - }); - - // 방에 참여중이지 않은 사용자의 경우, undefined을 반환합니다. - if (participantSubDocuments.length === 0) return undefined; - - // 방에 참여중인 사용자의 경우, 정산 상태가 완료된 것인지("paid"거나 "sent"인지)를 반환합니다. - return ["paid", "sent"].includes(participantSubDocuments[0].settlementStatus); -}; - -module.exports = { - roomPopulateOption, - formatSettlement, - getIsOver, -}; diff --git a/src/modules/populates/rooms.ts b/src/modules/populates/rooms.ts new file mode 100644 index 00000000..8fa42fbf --- /dev/null +++ b/src/modules/populates/rooms.ts @@ -0,0 +1,93 @@ +import { type User, type SettlementStatus, type Participant, type Room, type Location } from "@/types/mongo"; + +/** + * 쿼리를 통해 얻은 Room Document를 populate할 설정값을 정의합니다. + * @constant {{path: string, select: string, populate?: {path: string, select: string}}[]} + */ +export const roomPopulateOption = [ + { path: "from", select: "_id koName enName" }, + { path: "to", select: "_id koName enName" }, + { + path: "part", + select: "-_id user settlementStatus readAt", + populate: { path: "user", select: "_id id name nickname profileImageUrl" }, + }, +]; + +interface PopulatedParticipant extends Pick { + user: Pick; +} + +export interface PopulatedRoom extends Omit { + from: Pick; + to: Pick; + part?: PopulatedParticipant[]; +} + +export interface FormattedRoom extends Omit { + part?: { + _id: string; + name: string; + nickname: string; + profileImageUrl: string; + isSettlement?: SettlementStatus; + readAt: Date; + }[]; + settlementTotal?: number; + isOver?: boolean; + isDeparted: boolean; +}; + +/** + * Room Object가 주어졌을 때 room의 part array의 각 요소를 API 명세에서와 같이 {userId: String, ... , isSettlement: String}으로 가공합니다. + * 또한, 방이 현재 출발했는지 유무인 isDeparted 속성을 추가합니다. + * @param {PopulatedRoom} roomObject - 정산 정보를 가공할 room Object로, Mongoose Document가 아닌 순수 Javascript Object여야 합니다. + * @param {Object} options - 추가 파라미터로, 기본값은 {}입니다. + * @param {Boolean} options.includeSettlement - 반환 결과에 정산 정보를 포함할 지 여부로, 기본값은 true입니다. + * @param {Date} options.timestamp - 방의 출발 여부(isDeparted)를 판단하는 기준이 되는 시각입니다. + * @param {Boolean} options.isOver - 방의 완료 여부(isOver)로, 기본값은 false입니다. includeSettlement가 false인 경우 roomDocument의 isOver 속성은 undefined로 설정됩니다. + * @return {FormattedRoom} 정산 여부가 위와 같이 가공되고 isDeparted 속성이 추가된 Room Object가 반환됩니다. + */ +export const formatSettlement = ( + roomObject: PopulatedRoom, + { includeSettlement = true, isOver = false, timestamp = Date.now() } = {} +): FormattedRoom => { + return { + ...roomObject, + part: roomObject.part?.map((participantSubDocument) => { + const { _id, name, nickname, profileImageUrl } = + participantSubDocument.user; + const { settlementStatus, readAt } = participantSubDocument; + return { + _id, + name, + nickname, + profileImageUrl, + isSettlement: includeSettlement ? settlementStatus : undefined, + readAt: readAt ?? roomObject.madeat, + }; + }), + settlementTotal: includeSettlement ? roomObject.settlementTotal : undefined, + isOver: includeSettlement ? isOver : undefined, + isDeparted: new Date(roomObject.time) < new Date(timestamp), + }; +} + +/** + * roomPopulateOption을 사용해 populate된 Room Object와 사용자의 id(userId)가 주어졌을 때, 해당 사용자의 정산 상태를 반환합니다. + * @param {PopulatedRoom} roomObject - roomPopulateOption을 사용해 populate된 변환한 Room Object입니다. + * @param {String} userId - 방 완료 상태를 확인하려는 사용자의 id(user.id)입니다. + * @return {Boolean | undefined} 사용자의 해당 방에 대한 완료 여부(true | false)를 반환합니다. 사용자가 참여중인 방이 아닐 경우 undefined를 반환합니다. + **/ +export const getIsOver = (roomObject: PopulatedRoom, userId: string) => { + // room document의 part subdoocument에서 사용자 id와 일치하는 정산 정보를 찾습니다. + const participantSubDocuments = roomObject.part?.filter((part) => { + return part.user.id === userId; + }); + + // 방에 참여중이지 않은 사용자의 경우, undefined을 반환합니다. + if (!participantSubDocuments || participantSubDocuments.length === 0) return undefined; + + // 방에 참여중인 사용자의 경우, 정산 상태가 완료된 것인지("paid"거나 "sent"인지)를 반환합니다. + return ["paid", "sent"].includes(participantSubDocuments[0].settlementStatus); +}; diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index 810755ce..c05894df 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -1,6 +1,6 @@ -import { Types } from "mongoose"; +import { Document, Types } from "mongoose"; -export interface User { +export interface User extends Document { /** 사용자의 실명. */ name: string; /** 사용자의 닉네임. */ @@ -39,7 +39,7 @@ export interface User { export type SettlementStatus = "not-departed" | "paid" | "send-required" | "sent"; -export interface Participant { +export interface Participant extends Document { /** 방 참여자의 User ObjectID. */ user: Types.ObjectId; /** 방 참여자의 정산 상태. */ @@ -48,14 +48,14 @@ export interface Participant { readAt?: Date; } -export interface DeviceToken { +export interface DeviceToken extends Document { /** 디바이스 토큰 소유자의 User ObjectID. */ userId: Types.ObjectId; /** 소유한 디바이스 토큰의 배열. */ deviceTokens: Types.Array; } -export interface NotificationOption { +export interface NotificationOption extends Document { deviceToken: string; /** 채팅 알림 수신 여부. */ chatting: boolean; @@ -69,13 +69,13 @@ export interface NotificationOption { advertisement: boolean; } -export interface TopicSubscription { +export interface TopicSubscription extends Document { deviceToken?: string; topic?: string; subscribedAt: Date; } -export interface Room { +export interface Room extends Document { /** 방의 이름. */ name: string; /** 방의 출발지의 Location ObjectID. */ @@ -94,7 +94,7 @@ export interface Room { maxPartLength: number; } -export interface Location { +export interface Location extends Document { enName: string; koName: string; priority: number; @@ -116,7 +116,7 @@ export type ChatType = | "departure" | "arrival"; -export interface Chat { +export interface Chat extends Document { /** 메세지가 전송된 방의 Room ObjectID. */ roomId: Types.ObjectId; /** 메세지의 종류. */ @@ -128,7 +128,7 @@ export interface Chat { isValid: boolean; } -export interface Report { +export interface Report extends Document { /** 신고한 사용자의 ObjectID. */ creatorId: Types.ObjectId; /** 신고받은 사용자의 ObjectID. */ @@ -143,14 +143,14 @@ export interface Report { roomId?: Types.ObjectId; } -export interface AdminIPWhitelist { +export interface AdminIPWhitelist extends Document { ip: string; description: string; } export type AdminLogAction = "create" | "read" | "update" | "delete"; -export interface AdminLog { +export interface AdminLog extends Document { /** 로그 발생자의 User ObjectID. */ user: Types.ObjectId; /** 로그의 발생 시각. */ From 73fc564a56558808f1ce5a304d4799f549559096 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 6 Feb 2024 14:26:52 +0900 Subject: [PATCH 022/151] Fix: prettier TypeScript parsing error --- .prettierrc.json | 2 +- src/middlewares/errorHandler.ts | 2 +- src/middlewares/information.ts | 4 +++- src/middlewares/responseTime.ts | 2 +- src/middlewares/session.ts | 6 +++--- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index 873dba77..8be05812 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -16,5 +16,5 @@ "trailingComma": "es5", "useTabs": false, "vueIndentScriptAndStyle": false, - "parser": "babel" + "parser": "typescript" } diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 1b8c26ca..e0a7ba72 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,5 +1,5 @@ -import logger from "@/modules/logger"; import { type Request, type Response, type NextFunction } from "express"; +import logger from "@/modules/logger"; /** * Express app에서 사용할 custom global error handler를 정의합니다. diff --git a/src/middlewares/information.ts b/src/middlewares/information.ts index 4d209eec..0006ea40 100644 --- a/src/middlewares/information.ts +++ b/src/middlewares/information.ts @@ -5,7 +5,9 @@ const informationMiddleware = ( res: Response, next: NextFunction ) => { - req.clientIP = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + req.clientIP = + (req.headers["x-forwarded-for"] as string | undefined) || + req.connection.remoteAddress; req.timestamp = Date.now(); next(); }; diff --git a/src/middlewares/responseTime.ts b/src/middlewares/responseTime.ts index aec93458..a1bd8c54 100644 --- a/src/middlewares/responseTime.ts +++ b/src/middlewares/responseTime.ts @@ -1,6 +1,6 @@ import { type Request, type Response } from "express"; -import logger from "@/modules/logger"; import responseTime from "response-time"; +import logger from "@/modules/logger"; const responseTimeMiddleware = responseTime( (req: Request, res: Response, time: number) => { diff --git a/src/middlewares/session.ts b/src/middlewares/session.ts index 5a49e208..8cf0dcf8 100644 --- a/src/middlewares/session.ts +++ b/src/middlewares/session.ts @@ -1,10 +1,10 @@ import expressSession from "express-session"; import { nodeEnv, session as sessionConfig } from "@/loadenv"; -import sessionStore from "@/modules/stores/sessionStore"; import { type LoginInfo } from "@/modules/auths/login"; +import sessionStore from "@/modules/stores/sessionStore"; // 세션에 저장할 데이터 타입을 지정합니다. -declare module 'express-session' { +declare module "express-session" { interface SessionData { /** 사용자 로그인 정보 */ loginInfo?: LoginInfo; @@ -37,4 +37,4 @@ const sessionMiddleware = expressSession({ }, }); -export default sessionMiddleware; \ No newline at end of file +export default sessionMiddleware; From 4af19ff01bcaf4a8a8e4c685ab67938eb5411fb3 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 6 Feb 2024 15:31:18 +0900 Subject: [PATCH 023/151] Refactor: apply prettier --- src/modules/fcm.ts | 38 +++++++++++++++++++++++++----- src/modules/populates/chats.ts | 2 +- src/modules/populates/reports.ts | 7 ++++-- src/modules/populates/rooms.ts | 21 ++++++++++++----- src/modules/stores/mongo.ts | 26 ++++++++++++++++---- src/modules/stores/sessionStore.ts | 8 +++++-- src/types/index.d.ts | 2 +- src/types/mongo.d.ts | 14 +++++++---- 8 files changed, 90 insertions(+), 28 deletions(-) diff --git a/src/modules/fcm.ts b/src/modules/fcm.ts index 44074d7f..24418331 100644 --- a/src/modules/fcm.ts +++ b/src/modules/fcm.ts @@ -30,7 +30,10 @@ export const initializeApp = () => { * @param {string} deviceToken - 등록하려는 FCM device token입니다. * @return {Promise>} 변경된 사용자의 deviceToken의 목록 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ -export const registerDeviceToken = async (userId: string, deviceToken: string): Promise => { +export const registerDeviceToken = async ( + userId: string, + deviceToken: string +): Promise => { try { // 디바이스 토큰을 다른 사용자가 사용하고 있는지 확인 및 삭제합니다. await deviceTokenModel.updateMany( @@ -96,7 +99,10 @@ export const unregisterDeviceToken = async (deviceToken: string) => { * @param {Array} fcmResponses - 등록하려는 FCM device token입니다. * @return {Promise>} 각각의 토큰들의 삭제 성공 여부가 저장된 Array를 반환합니다. 해당 토큰을 DB에서 삭제하는 데 성공했으면 true, 아니면 false가 포함됩니다. */ -const removeExpiredTokens = async (deviceTokens: string[], fcmResponses: SendResponse[]) => { +const removeExpiredTokens = async ( + deviceTokens: string[], + fcmResponses: SendResponse[] +) => { const removalResults = await Promise.all( deviceTokens.map(async (deviceToken, index) => { try { @@ -151,7 +157,10 @@ export const validateDeviceToken = async (deviceToken: string) => { * @param {Boolean?} notificationOptions.chatting - true 또는 false로 주어진 경우, 채팅 알림 설정이 각각 true 또는 false로 설정된 사용자들의 deviceToken만 반환합니다. * @return {Promise>} deviceToken의 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ -export const getTokensOfUsers = async (userIds: string[], notificationOptions: Object = {}) => { +export const getTokensOfUsers = async ( + userIds: string[], + notificationOptions: Object = {} +) => { const deviceTokensOfUsers = ( await Promise.all( userIds.map( @@ -186,7 +195,14 @@ export const getTokensOfUsers = async (userIds: string[], notificationOptions: O * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. * @return {Promise} 메시지 알림 전송에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -export const sendMessageByTokens = async (tokens: string[], type: ChatType, title: string, body: string, icon?: string, link?: string) => { +export const sendMessageByTokens = async ( + tokens: string[], + type: ChatType, + title: string, + body: string, + icon?: string, + link?: string +) => { if (tokens.length === 0) return -1; try { const message = { @@ -227,7 +243,14 @@ export const sendMessageByTokens = async (tokens: string[], type: ChatType, titl * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. * @return {Promise} 메시지 알림 전송에 성공했으면 true, 아니면 false를 반환합니다. */ -export const sendMessageByTopic = async (topic: string, type: ChatType, title: string, body: string, icon?: string, link?: string) => { +export const sendMessageByTopic = async ( + topic: string, + type: ChatType, + title: string, + body: string, + icon?: string, + link?: string +) => { try { const message = { topic, @@ -305,7 +328,10 @@ export const subscribeUserToTopic = async (userId: string, topic: string) => { * @param {string} topic - 구독을 해제할 topic입니다. * @return {Promise} 토픽 구독 해제에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -export const unsubscribeUserFromTopic = async (userId: string, topic: string) => { +export const unsubscribeUserFromTopic = async ( + userId: string, + topic: string +) => { try { const deviceToken = await deviceTokenModel.findOne({ userId, diff --git a/src/modules/populates/chats.ts b/src/modules/populates/chats.ts index fa92f2d6..633dd174 100644 --- a/src/modules/populates/chats.ts +++ b/src/modules/populates/chats.ts @@ -9,4 +9,4 @@ export const chatPopulateOption = [ export interface PopulatedChat extends Omit { authorId?: Pick; -}; +} diff --git a/src/modules/populates/reports.ts b/src/modules/populates/reports.ts index 439311a7..a9fe3c9e 100644 --- a/src/modules/populates/reports.ts +++ b/src/modules/populates/reports.ts @@ -8,5 +8,8 @@ export const reportPopulateOption = [ ]; export interface PopulatedReport extends Omit { - reportedId: Pick; -}; + reportedId: Pick< + User, + "_id" | "id" | "name" | "nickname" | "profileImageUrl" + >; +} diff --git a/src/modules/populates/rooms.ts b/src/modules/populates/rooms.ts index 8fa42fbf..15f39338 100644 --- a/src/modules/populates/rooms.ts +++ b/src/modules/populates/rooms.ts @@ -1,4 +1,10 @@ -import { type User, type SettlementStatus, type Participant, type Room, type Location } from "@/types/mongo"; +import { + type User, + type SettlementStatus, + type Participant, + type Room, + type Location, +} from "@/types/mongo"; /** * 쿼리를 통해 얻은 Room Document를 populate할 설정값을 정의합니다. @@ -14,7 +20,8 @@ export const roomPopulateOption = [ }, ]; -interface PopulatedParticipant extends Pick { +interface PopulatedParticipant + extends Pick { user: Pick; } @@ -24,7 +31,8 @@ export interface PopulatedRoom extends Omit { part?: PopulatedParticipant[]; } -export interface FormattedRoom extends Omit { +export interface FormattedRoom + extends Omit { part?: { _id: string; name: string; @@ -36,7 +44,7 @@ export interface FormattedRoom extends Omit { }); // 방에 참여중이지 않은 사용자의 경우, undefined을 반환합니다. - if (!participantSubDocuments || participantSubDocuments.length === 0) return undefined; + if (!participantSubDocuments || participantSubDocuments.length === 0) + return undefined; // 방에 참여중인 사용자의 경우, 정산 상태가 완료된 것인지("paid"거나 "sent"인지)를 반환합니다. return ["paid", "sent"].includes(participantSubDocuments[0].settlementStatus); diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index bc248bce..51c69c15 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -1,6 +1,18 @@ import mongoose, { model, Schema, Types } from "mongoose"; import logger from "@/modules/logger"; -import type { User, Participant, DeviceToken, NotificationOption, TopicSubscription, Room, Location, Chat, Report, AdminIPWhitelist, AdminLog } from "@/types/mongo"; +import type { + User, + Participant, + DeviceToken, + NotificationOption, + TopicSubscription, + Room, + Location, + Chat, + Report, + AdminIPWhitelist, + AdminLog, +} from "@/types/mongo"; const userSchema = new Schema({ name: { type: String, required: true }, //실명 @@ -217,17 +229,21 @@ export const connectDatabase = (mongoUrl: string) => { // 데이터베이스 연결이 끊어지면 5초 후 재연결을 시도합니다. logger.error("데이터베이스와 연결이 끊어졌습니다!"); setTimeout(() => { - mongoose.connect(mongoUrl, /*{ + mongoose.connect( + mongoUrl /*{ useNewUrlParser: true, useUnifiedTopology: true, - }*/); // NOTE: https://velog.io/@untiring_dev/MongoDB-MongoDB-Mongoose%EC%97%90-%EC%97%B0%EA%B2%B0 + }*/ + ); // NOTE: https://velog.io/@untiring_dev/MongoDB-MongoDB-Mongoose%EC%97%90-%EC%97%B0%EA%B2%B0 }, 5000); }); - mongoose.connect(mongoUrl, /*{ + mongoose.connect( + mongoUrl /*{ useNewUrlParser: true, useUnifiedTopology: true, - }*/); + }*/ + ); return database; }; diff --git a/src/modules/stores/sessionStore.ts b/src/modules/stores/sessionStore.ts index 11647e00..11ec9a78 100644 --- a/src/modules/stores/sessionStore.ts +++ b/src/modules/stores/sessionStore.ts @@ -1,7 +1,11 @@ import MongoStore from "connect-mongo"; -import RedisStore from "connect-redis" +import RedisStore from "connect-redis"; import redis from "redis"; -import { redis as redisUrl, mongo as mongoUrl, session as sessionConfig } from "@/loadenv"; +import { + redis as redisUrl, + mongo as mongoUrl, + session as sessionConfig, +} from "@/loadenv"; import logger from "@/modules/logger"; const getSessionStore = () => { diff --git a/src/types/index.d.ts b/src/types/index.d.ts index acee1ace..d679d420 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -16,4 +16,4 @@ declare global { timestamp?: number; } } -} \ No newline at end of file +} diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index c05894df..1f0bf64f 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -24,10 +24,10 @@ export interface User extends Document { agreeOnTermsOfService: boolean; subinfo?: { /** 사용자의 KAIST 학번. */ - kaist: string, - sparcs: string, - facebook: string, - twitter: string, + kaist: string; + sparcs: string; + facebook: string; + twitter: string; }; /** 사용자의 이메일 주소. */ email: string; @@ -37,7 +37,11 @@ export interface User extends Document { account: string; } -export type SettlementStatus = "not-departed" | "paid" | "send-required" | "sent"; +export type SettlementStatus = + | "not-departed" + | "paid" + | "send-required" + | "sent"; export interface Participant extends Document { /** 방 참여자의 User ObjectID. */ From 6b3ecf67ea70c7796c259c9c021c3f0bb9291cb0 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 6 Feb 2024 21:14:38 +0900 Subject: [PATCH 024/151] =?UTF-8?q?Add:=20=EC=B2=AB=EB=B2=88=EC=A7=B8=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=BD=94=EB=93=9C=20(=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=98=88=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 1 + loadenv.js | 4 ++ src/locations.json | 19 +++++ src/modules/fare.js | 0 src/modules/stores/mongo.js | 13 ++++ src/routes/fare.js | 28 ++++++++ src/services/fare.js | 138 ++++++++++++++++++++++++++++++++++++ 7 files changed, 203 insertions(+) create mode 100644 src/locations.json create mode 100644 src/modules/fare.js create mode 100644 src/routes/fare.js create mode 100644 src/services/fare.js diff --git a/app.js b/app.js index a26c4b46..dcab200b 100644 --- a/app.js +++ b/app.js @@ -69,6 +69,7 @@ app.use("/chats", require("./src/routes/chats")); app.use("/locations", require("./src/routes/locations")); app.use("/reports", require("./src/routes/reports")); app.use("/notifications", require("./src/routes/notifications")); +app.use("/fare", require("./src/routes/fare")); // [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. app.use(require("./src/middlewares/errorHandler")); diff --git a/loadenv.js b/loadenv.js index 789e21db..64b57b19 100644 --- a/loadenv.js +++ b/loadenv.js @@ -44,4 +44,8 @@ module.exports = { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional + + // Naver Cloud Platform Maps Directions 5 API Keys + naverCloudApiId: process.env.NAVER_MAP_API_ID, //required + naverCloudApiKey: process.env.NAVER_MAP_API_KEY, //required }; diff --git a/src/locations.json b/src/locations.json new file mode 100644 index 00000000..73e5cc21 --- /dev/null +++ b/src/locations.json @@ -0,0 +1,19 @@ +{ + "카이스트 본원": "127.357913,36.373724", + "카이스트 문지캠퍼스": "127.396729,36.393039", + "대전역": "127.432885,36.331373", + "서대전역": "127.404002,36.322479", + "대전복합터미널": "127.436362,36.351862", + "유성 고속버스터미널": "127.336115,36.359945", + "유성 시외버스터미널": "127.330246,36.356030", + "대전청사 고속버스터미널": "127.388202,36.361294", + "대전청사 시외버스터미널": "127.377567,36.361290", + "갤러리아 타임월드": "127.378191,36.352513", + "궁동 로데오거리": "127.349825,36.360342", + "만년중학교": "127.3757119,36.366771", + "신세계백화점": "127.382517,36.3779922", + "월평역": "127.368200,36.3580706", + "유성구청": "127.355899,36.362081", + "유성구 예비군 훈련장": "127.341971,36.393751" + } + \ No newline at end of file diff --git a/src/modules/fare.js b/src/modules/fare.js new file mode 100644 index 00000000..e69de29b diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 695845fc..0000e74c 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -172,6 +172,18 @@ const adminLogSchema = Schema({ }, // 수행 업무 }); +const taxiFareSchema = Schema( + { + start: { type: String, required: true }, // 출발지 + goal: { type: String, required: true }, // 도착지 + time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리, 0 ~ 47 (0:00 ~ 23:30)) + fare: { type: Number, default: false }, // 예상 택시 요금 + }, + { + timestamps: true, // 최근 업데이트 시간 기록용 + } +); + mongoose.set("strictQuery", true); const database = mongoose.connection; @@ -225,4 +237,5 @@ module.exports = { adminIPWhitelistSchema ), adminLogModel: mongoose.model("AdminLog", adminLogSchema), + taxiFareModel: mongoose.model("TaxiFare", taxiFareSchema), }; diff --git a/src/routes/fare.js b/src/routes/fare.js new file mode 100644 index 00000000..fbe7d2c5 --- /dev/null +++ b/src/routes/fare.js @@ -0,0 +1,28 @@ +const express = require("express"); +const { check } = require("express-validator"); +const router = express.Router(); + +const { validator } = require("../middlewares/validator"); +const { getTaxiFare, initDatabase } = require("../services/fare"); +const locations = require("../locations.json"); + +const checkTaxiFareParams = [ + check("start") + .isIn(Object.keys(locations)) //TODO: Change Location style of taxi-fare to match taxi-back + .withMessage("출발지가 올바르지 않습니다"), + check("goal") + .isIn(Object.keys(locations)) //TODO: Change Location style of taxi-fare to match taxi-back + .withMessage("도착지가 올바르지 않습니다"), + check("time") + .exists() + .withMessage("날짜/시간을 입력해주세요") + .isISO8601() + .withMessage("날짜/시간 형식이 올바르지 않습니다"), + validator, +]; + +router.get("/init", initDatabase); + +router.get("/:start-:goal/time/:time", getTaxiFare); + +module.exports = router; diff --git a/src/services/fare.js b/src/services/fare.js new file mode 100644 index 00000000..cdd65ba1 --- /dev/null +++ b/src/services/fare.js @@ -0,0 +1,138 @@ +const axios = require("axios"); + +const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); +const { taxiFareModel } = require("../modules/stores/mongo"); +const locations = require("../locations.json"); //TODO: Change Location style of taxi-fare to match taxi-back + +// Naver Cloud Platform Maps Directions 5 API Keys +const naverCloudApi = { + "X-NCP-APIGW-API-KEY-ID": naverCloudApiId, + "X-NCP-APIGW-API-KEY": naverCloudApiKey, +}; + +// Initialize database +// Erase all previous data and sets all taxi fare to 0 +const initDatabase = async (req, res) => { + try { + // Remove all previous data + await taxiFareModel.deleteMany({}); + + //TODO: Change Location style of taxi-fare to match taxi-back + for (let skey in locations) { + //TODO: Change Location style of taxi-fare to match taxi-back + for (let gkey in locations) { + if (skey === gkey) continue; + let tableFare = []; + if ( + (skey === "카이스트 본원" && gkey === "대전역") || + (skey === "대전역" && gkey === "카이스트 본원") + ) { + for (let i = 0; i < 48; i++) { + tableFare.push({ + start: skey, + goal: gkey, + time: i, + fare: 0, + }); + } + } else { + tableFare.push({ + start: skey, + goal: gkey, + time: 0, + fare: 0, + }); + } + await taxiFareModel.insertMany(tableFare); + } + } + res.state(200).json({ message: "TaxiFare Database initialized" }); + } catch (err) { + res.status(500).json({ error: "Failed with exception " + err.message }); + } +}; + +/** + * 주어진 start, goal, time에 대한 택시 요금을 반환합니다. + * @param {Request} req - 파라미터로 start, goal, time을 받습니다. + * - @param {String} start - 출발지 + * - @param {String} goal - 도착지 + * - @param {Date} time - 출발 시간 (ISO 8601) + */ +const getTaxiFare = async (req, res) => { + try { + let start = locations[req.params.start]; //TODO: Change Location style of taxi-fare to match taxi-back + let goal = locations[req.params.goal]; //TODO: Change Location style of taxi-fare to match taxi-back + let time = new Date(req.params.time); + let sTime = time.getHours() * 2 + Math.floor(time.getMinutes() / 30); // Scaled Time. 0 ~ 47 (0:00 ~ 23:30) + + console.log(taxiFareModel); + let taxiFare = await taxiFareModel + .findOne( + { + start: req.params.start, + goal: req.params.goal, + time: sTime, + }, + function (err, docs) { + if (err) + console.log( + "Error occured while finding TaxiFare documents: " + err.message + ); + } + ) + .clone(); + + if ( + taxiFare && + new Date() - taxiFare.updatedAt < 24 * 60 * 60 * 1000 && + taxiFare.fare !== 0 + ) { + res.json({ fare: taxiFare.fare }); + } else { + let response = await axios.get( + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${start}&goal=${goal}&options=traoptimal`, + { headers: naverCloudApi } + ); + + let fare = response.data.route.traoptimal[0].summary.taxiFare; + if (!taxiFare) { + taxiFare = new taxiFareModel( + { + start: req.params.start, + goal: req.params.goal, + time: sTime, + fare: fare, + }, + function (err, docs) { + if (err) + console.log( + "Error occured while creating a new document of TaxiFare: " + + err.message + ); + } + ); // 만일 document가 중간에 삭제되어 공백이 생겼을 경우 채우는 용도 + } else { + await taxiFareModel + .updateOne( + { start: req.params.start, goal: req.params.goal, time: sTime }, + { fare: fare }, + function (err, docs) { + if (err) + console.log( + "Error occured while updating TaxiFare document: " + + err.message + ); + } + ) + .clone(); + } + res.json({ fare: fare }); + } + } catch (err) { + console.log(err); + res.status(500).json({ error: "Failed with exception: " + err.message }); + } +}; + +module.exports = { initDatabase, getTaxiFare }; From 1d7a1066f517e55a5582da034658bad5a8d396bb Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 6 Feb 2024 21:55:23 +0900 Subject: [PATCH 025/151] =?UTF-8?q?Add:=20Cron=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EC=9E=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/fare.js | 10 +++ src/schedules/index.js | 2 + src/schedules/updateMajorTaxiFare.js | 16 ++++ src/schedules/updateMinorTaxiFare.js | 23 +++++ src/services/fare.js | 128 ++++++++++++++++----------- 5 files changed, 127 insertions(+), 52 deletions(-) create mode 100644 src/schedules/updateMajorTaxiFare.js create mode 100644 src/schedules/updateMinorTaxiFare.js diff --git a/src/modules/fare.js b/src/modules/fare.js index e69de29b..f77d62fc 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -0,0 +1,10 @@ +/* + * 시간을 받아서 30분 단위로 변환해서 반환합니다. + * 00:00 ~ 23:59 -> 0 ~ 47 + * @param {Date} time 변환할 시간 + */ +const scaledTime = (time) => { + return time.getHours() * 2 + (time.getMinutes() >= 30 ? 1 : 0); +}; + +module.exports = { scaledTime }; diff --git a/src/schedules/index.js b/src/schedules/index.js index 97818b92..fda57d53 100644 --- a/src/schedules/index.js +++ b/src/schedules/index.js @@ -3,6 +3,8 @@ const cron = require("node-cron"); const registerSchedules = (app) => { cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); + cron.schedule("0,30 * * * * ", require("./updateMajorTaxiFare")(app)); + cron.schedule("0 18 * * *", require("./updateMinorTaxiFare")(app)); }; module.exports = registerSchedules; diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js new file mode 100644 index 00000000..a21f305f --- /dev/null +++ b/src/schedules/updateMajorTaxiFare.js @@ -0,0 +1,16 @@ +const { scaledTime } = require("../modules/fare"); +const { updateTaxiFare } = require("../services/fare"); + +/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 캐싱합니다. */ +module.exports = (app) => async () => { + try { + start = "카이스트 본원"; + goal = "대전역"; + time = new Date(); + sTime = scaledTime(time); + await updateTaxiFare(start, goal, sTime); + await updateTaxiFare(goal, start, sTime); + } catch (err) { + console.log(err); + } +}; diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.js new file mode 100644 index 00000000..9e0d5dda --- /dev/null +++ b/src/schedules/updateMinorTaxiFare.js @@ -0,0 +1,23 @@ +const { updateTaxiFare } = require("../services/fare"); +const locations = require("../locations.json"); //TODO: Change Location style of taxi-fare to match taxi-back + +/* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 캐싱합니다. */ +module.exports = (app) => async () => { + try { + for (let locStart in locations) { + for (let locGoal in locations) { + if (locStart === locGoal) continue; + if ( + (locStart === "카이스트 본원" && locGoal === "대전역") || + (locStart === "대전역" && locGoal === "카이스트 본원") + ) + continue; + else { + await updateTaxiFare(locStart, locGoal, 0); //18:00시의 택시 요금이지만 db에는 0으로 저장됨 + } + } + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/services/fare.js b/src/services/fare.js index cdd65ba1..1e189821 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -2,6 +2,7 @@ const axios = require("axios"); const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); const { taxiFareModel } = require("../modules/stores/mongo"); +const { scaledTime } = require("../modules/fare"); const locations = require("../locations.json"); //TODO: Change Location style of taxi-fare to match taxi-back // Naver Cloud Platform Maps Directions 5 API Keys @@ -10,8 +11,12 @@ const naverCloudApi = { "X-NCP-APIGW-API-KEY": naverCloudApiKey, }; -// Initialize database -// Erase all previous data and sets all taxi fare to 0 +/* Initialize database + * 1. Erase all previous data + * 2. Sets all taxi fare to 0 + * 카이스트 본원 <-> 대전역의 경우 48개의 시간대에 대한 택시 요금을 0으로 설정합니다. + * 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 time과 fare를 0으로 설정합니다. + */ const initDatabase = async (req, res) => { try { // Remove all previous data @@ -54,6 +59,8 @@ const initDatabase = async (req, res) => { /** * 주어진 start, goal, time에 대한 택시 요금을 반환합니다. + * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 매일 18:00시의 택시 요금을 반환합니다. + * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. * @param {Request} req - 파라미터로 start, goal, time을 받습니다. * - @param {String} start - 출발지 * - @param {String} goal - 도착지 @@ -64,11 +71,13 @@ const getTaxiFare = async (req, res) => { let start = locations[req.params.start]; //TODO: Change Location style of taxi-fare to match taxi-back let goal = locations[req.params.goal]; //TODO: Change Location style of taxi-fare to match taxi-back let time = new Date(req.params.time); - let sTime = time.getHours() * 2 + Math.floor(time.getMinutes() / 30); // Scaled Time. 0 ~ 47 (0:00 ~ 23:30) + let sTime = scaledTime(time); // Scaled Time. 0 ~ 47 (0:00 ~ 23:30) - console.log(taxiFareModel); - let taxiFare = await taxiFareModel - .findOne( + if ( + (req.params.start === "카이스트 본원" && req.params.goal === "대전역") || + (req.params.start === "대전역" && req.params.goal === "카이스트 본원") + ) { + let taxiFare = await taxiFareModel.findOne( { start: req.params.start, goal: req.params.goal, @@ -80,54 +89,43 @@ const getTaxiFare = async (req, res) => { "Error occured while finding TaxiFare documents: " + err.message ); } - ) - .clone(); - - if ( - taxiFare && - new Date() - taxiFare.updatedAt < 24 * 60 * 60 * 1000 && - taxiFare.fare !== 0 - ) { - res.json({ fare: taxiFare.fare }); - } else { - let response = await axios.get( - `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${start}&goal=${goal}&options=traoptimal`, - { headers: naverCloudApi } ); - - let fare = response.data.route.traoptimal[0].summary.taxiFare; - if (!taxiFare) { - taxiFare = new taxiFareModel( - { - start: req.params.start, - goal: req.params.goal, - time: sTime, - fare: fare, - }, - function (err, docs) { - if (err) - console.log( - "Error occured while creating a new document of TaxiFare: " + - err.message - ); - } - ); // 만일 document가 중간에 삭제되어 공백이 생겼을 경우 채우는 용도 + if (taxiFare.fare === 0) { + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + let response = await axios.get( + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${start}&goal=${goal}&options=traoptimal`, + { headers: naverCloudApi } + ); + res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); } else { - await taxiFareModel - .updateOne( - { start: req.params.start, goal: req.params.goal, time: sTime }, - { fare: fare }, - function (err, docs) { - if (err) - console.log( - "Error occured while updating TaxiFare document: " + - err.message - ); - } - ) - .clone(); + res.json({ fare: taxiFare.fare }); + } + } + // 카이스트 본원 <-> 대전역이 아닌 경우 + else { + let taxiFare = await taxiFareModel.findOne( + { + start: req.params.start, + goal: req.params.goal, + time: 0, + }, + function (err, docs) { + if (err) + console.log( + "Error occured while finding TaxiFare documents: " + err.message + ); + } + ); + if (taxiFare.fare === 0) { + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + let response = await axios.get( + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${start}&goal=${goal}&options=traoptimal`, + { headers: naverCloudApi } + ); + res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); + } else { + res.json({ fare: taxiFare.fare }); } - res.json({ fare: fare }); } } catch (err) { console.log(err); @@ -135,4 +133,30 @@ const getTaxiFare = async (req, res) => { } }; -module.exports = { initDatabase, getTaxiFare }; +/** + * 주어진 start, goal, sTime에 대한 택시 요금을 업데이트합니다. + * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. + * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. + * @param {String} locStart - 출발지 string + * @param {String} locGoal - 도착지 string + * @param {Date} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 47 (0:00 ~ 23:59)) + */ +const updateTaxiFare = async (locStart, locGoal, sTime) => { + let response = await axios.get( + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${locations[locStart]}&goal=${locations[locGoal]}&options=traoptimal`, + { headers: naverCloudApi } + ); + let fare = response.data.route.traoptimal[0].summary.taxiFare; + await taxiFareModel.updateOne( + { start: locStart, goal: locGoal, time: sTime }, + { fare: fare }, + function (err, docs) { + if (err) + console.log( + "Error occured while updating TaxiFare document: " + err.message + ); + } + ); +}; + +module.exports = { initDatabase, getTaxiFare, updateTaxiFare }; From 4de79263a6d499b662f1aa2a438ea0e73914063b Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 6 Feb 2024 22:10:16 +0900 Subject: [PATCH 026/151] =?UTF-8?q?Docs:=20=EC=A3=BC=EC=84=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/fare.js | 2 +- src/services/fare.js | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index f77d62fc..9dc1fe43 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -1,4 +1,4 @@ -/* +/** * 시간을 받아서 30분 단위로 변환해서 반환합니다. * 00:00 ~ 23:59 -> 0 ~ 47 * @param {Date} time 변환할 시간 diff --git a/src/services/fare.js b/src/services/fare.js index 1e189821..8e7ed2cc 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -11,11 +11,11 @@ const naverCloudApi = { "X-NCP-APIGW-API-KEY": naverCloudApiKey, }; -/* Initialize database +/** Initialize database * 1. Erase all previous data * 2. Sets all taxi fare to 0 - * 카이스트 본원 <-> 대전역의 경우 48개의 시간대에 대한 택시 요금을 0으로 설정합니다. - * 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 time과 fare를 0으로 설정합니다. + * @summary 카이스트 본원 <-> 대전역의 경우 48개의 시간대에 대한 택시 요금을 0으로 설정합니다. + * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 time과 fare를 0으로 설정합니다. */ const initDatabase = async (req, res) => { try { @@ -28,6 +28,7 @@ const initDatabase = async (req, res) => { for (let gkey in locations) { if (skey === gkey) continue; let tableFare = []; + // 카이스트 본원 <-> 대전역의 경우 48개의 시간대에 대한 택시 요금을 0으로 설정 if ( (skey === "카이스트 본원" && gkey === "대전역") || (skey === "대전역" && gkey === "카이스트 본원") @@ -40,7 +41,9 @@ const initDatabase = async (req, res) => { fare: 0, }); } - } else { + } + // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 1개씩만 collection 지정 설정 + else { tableFare.push({ start: skey, goal: gkey, From 754396c7a68d205bcadd7cd2aecf838abaa496a4 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 6 Feb 2024 22:29:07 +0900 Subject: [PATCH 027/151] =?UTF-8?q?Add:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20ap?= =?UTF-8?q?i=EC=9A=A9=20.env=20=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index ea107da9..9e2688e9 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,8 @@ CORS_WHITELIST=[CORS 정책에서 허용하는 도메인의 목록(e.g. ["http:/ GOOGLE_APPLICATION_CREDENTIALS=[GOOGLE_APPLICATION_CREDENTIALS JSON] TEST_ACCOUNTS=[스팍스SSO로 로그인시 무조건 테스트로 로그인이 가능한 허용 아이디 목록] SLACK_REPORT_WEBHOOK_URL=[Slack 웹훅 URL들이 담긴 JSON] +NAVER_MAP_API_ID=[네이버 지도 API ID] +NAVER_MAP_API_KEY=[네이버 지도 API KEY] # optional environment variables for taxiSampleGenerator SAMPLE_NUM_OF_ROOMS=[방의 개수] From de8eefc2e05edb5be5e956425cce38bf0b9b6029 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 13 Feb 2024 17:35:17 +0900 Subject: [PATCH 028/151] Add: Added validator & use project locations --- src/locations.json | 19 ----- src/routes/fare.js | 46 +++++++----- src/schedules/updateMinorTaxiFare.js | 9 ++- src/services/fare.js | 106 ++++++++++++++++----------- 4 files changed, 96 insertions(+), 84 deletions(-) delete mode 100644 src/locations.json diff --git a/src/locations.json b/src/locations.json deleted file mode 100644 index 73e5cc21..00000000 --- a/src/locations.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "카이스트 본원": "127.357913,36.373724", - "카이스트 문지캠퍼스": "127.396729,36.393039", - "대전역": "127.432885,36.331373", - "서대전역": "127.404002,36.322479", - "대전복합터미널": "127.436362,36.351862", - "유성 고속버스터미널": "127.336115,36.359945", - "유성 시외버스터미널": "127.330246,36.356030", - "대전청사 고속버스터미널": "127.388202,36.361294", - "대전청사 시외버스터미널": "127.377567,36.361290", - "갤러리아 타임월드": "127.378191,36.352513", - "궁동 로데오거리": "127.349825,36.360342", - "만년중학교": "127.3757119,36.366771", - "신세계백화점": "127.382517,36.3779922", - "월평역": "127.368200,36.3580706", - "유성구청": "127.355899,36.362081", - "유성구 예비군 훈련장": "127.341971,36.393751" - } - \ No newline at end of file diff --git a/src/routes/fare.js b/src/routes/fare.js index fbe7d2c5..d03b560e 100644 --- a/src/routes/fare.js +++ b/src/routes/fare.js @@ -1,28 +1,36 @@ const express = require("express"); -const { check } = require("express-validator"); +const { query } = require("express-validator"); const router = express.Router(); -const { validator } = require("../middlewares/validator"); +const validator = require("../middlewares/validator"); const { getTaxiFare, initDatabase } = require("../services/fare"); -const locations = require("../locations.json"); - -const checkTaxiFareParams = [ - check("start") - .isIn(Object.keys(locations)) //TODO: Change Location style of taxi-fare to match taxi-back - .withMessage("출발지가 올바르지 않습니다"), - check("goal") - .isIn(Object.keys(locations)) //TODO: Change Location style of taxi-fare to match taxi-back - .withMessage("도착지가 올바르지 않습니다"), - check("time") - .exists() - .withMessage("날짜/시간을 입력해주세요") - .isISO8601() - .withMessage("날짜/시간 형식이 올바르지 않습니다"), - validator, -]; +const { locationModel } = require("../modules/stores/mongo"); router.get("/init", initDatabase); -router.get("/:start-:goal/time/:time", getTaxiFare); +router.get( + "/getTaxiFare", + async (req, res, next) => { + req.locations = ( + await locationModel.find({ isValid: { $ne: false } }, "koName") + ).map((location) => location.koName); + next(); + }, + query("start").custom((value, { req }) => { + if (!req.locations.includes(value)) { + throw new Error("Invalid start location"); + } + return true; + }), + query("goal").custom((value, { req }) => { + if (!req.locations.includes(value)) { + throw new Error("Invalid goal location"); + } + return true; + }), + query("time").isISO8601(), + validator, + getTaxiFare +); module.exports = router; diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.js index 9e0d5dda..66b2b2e9 100644 --- a/src/schedules/updateMinorTaxiFare.js +++ b/src/schedules/updateMinorTaxiFare.js @@ -1,11 +1,14 @@ const { updateTaxiFare } = require("../services/fare"); -const locations = require("../locations.json"); //TODO: Change Location style of taxi-fare to match taxi-back +const { locationModel } = require("../modules/stores/mongo"); /* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 캐싱합니다. */ module.exports = (app) => async () => { try { - for (let locStart in locations) { - for (let locGoal in locations) { + const location = ( + await locationModel.find({ isValid: { $ne: false } }, "koName") + ).map((location) => location.koName); + for (let locStart in location) { + for (let locGoal in location) { if (locStart === locGoal) continue; if ( (locStart === "카이스트 본원" && locGoal === "대전역") || diff --git a/src/services/fare.js b/src/services/fare.js index 8e7ed2cc..1944c912 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -1,9 +1,8 @@ const axios = require("axios"); const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); -const { taxiFareModel } = require("../modules/stores/mongo"); +const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); const { scaledTime } = require("../modules/fare"); -const locations = require("../locations.json"); //TODO: Change Location style of taxi-fare to match taxi-back // Naver Cloud Platform Maps Directions 5 API Keys const naverCloudApi = { @@ -22,10 +21,12 @@ const initDatabase = async (req, res) => { // Remove all previous data await taxiFareModel.deleteMany({}); - //TODO: Change Location style of taxi-fare to match taxi-back - for (let skey in locations) { - //TODO: Change Location style of taxi-fare to match taxi-back - for (let gkey in locations) { + const location = ( + await locationModel.find({ isValid: { $ne: false } }, "koName") + ).map((location) => location.koName); + + for (let skey in location) { + for (let gkey in location) { if (skey === gkey) continue; let tableFare = []; // 카이스트 본원 <-> 대전역의 경우 48개의 시간대에 대한 택시 요금을 0으로 설정 @@ -63,7 +64,7 @@ const initDatabase = async (req, res) => { /** * 주어진 start, goal, time에 대한 택시 요금을 반환합니다. * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 매일 18:00시의 택시 요금을 반환합니다. - * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. + * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 존재하지 않을 경우에는 직접 호출합니다. * @param {Request} req - 파라미터로 start, goal, time을 받습니다. * - @param {String} start - 출발지 * - @param {String} goal - 도착지 @@ -71,32 +72,41 @@ const initDatabase = async (req, res) => { */ const getTaxiFare = async (req, res) => { try { - let start = locations[req.params.start]; //TODO: Change Location style of taxi-fare to match taxi-back - let goal = locations[req.params.goal]; //TODO: Change Location style of taxi-fare to match taxi-back - let time = new Date(req.params.time); + let start = await locationModel.findOne({ + koName: { $eq: req.query.start }, + }); + let goal = await locationModel + .findOne({ koName: { $eq: req.query.goal } }) + .clone(); + let time = new Date(req.query.time); let sTime = scaledTime(time); // Scaled Time. 0 ~ 47 (0:00 ~ 23:30) + // 카이스트 본원 <-> 대전역 if ( - (req.params.start === "카이스트 본원" && req.params.goal === "대전역") || - (req.params.start === "대전역" && req.params.goal === "카이스트 본원") + (start.koName === "카이스트 본원" && goal.koName === "대전역") || + (start.koName === "대전역" && goal.koName === "카이스트 본원") ) { - let taxiFare = await taxiFareModel.findOne( - { - start: req.params.start, - goal: req.params.goal, - time: sTime, - }, - function (err, docs) { - if (err) - console.log( - "Error occured while finding TaxiFare documents: " + err.message - ); - } - ); + let taxiFare = await taxiFareModel + .findOne( + { + start: start.koName, + goal: goal.koName, + time: sTime, + }, + function (err, docs) { + if (err) + console.log( + "Error occured while finding TaxiFare documents: " + err.message + ); + } + ) + .clone(); + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (taxiFare.fare === 0) { - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 let response = await axios.get( - `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${start}&goal=${goal}&options=traoptimal`, + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ + start.longitude + "," + start.latitude + }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, { headers: naverCloudApi } ); res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); @@ -106,23 +116,27 @@ const getTaxiFare = async (req, res) => { } // 카이스트 본원 <-> 대전역이 아닌 경우 else { - let taxiFare = await taxiFareModel.findOne( - { - start: req.params.start, - goal: req.params.goal, - time: 0, - }, - function (err, docs) { - if (err) - console.log( - "Error occured while finding TaxiFare documents: " + err.message - ); - } - ); + let taxiFare = await taxiFareModel + .findOne( + { + start: start.koName, + goal: goal.koName, + time: 0, + }, + function (err, docs) { + if (err) + console.log( + "Error occured while finding TaxiFare documents: " + err.message + ); + } + ) + .clone(); + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (taxiFare.fare === 0) { - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 let response = await axios.get( - `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${start}&goal=${goal}&options=traoptimal`, + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ + start.longitude + "," + start.latitude + }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, { headers: naverCloudApi } ); res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); @@ -145,8 +159,14 @@ const getTaxiFare = async (req, res) => { * @param {Date} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 47 (0:00 ~ 23:59)) */ const updateTaxiFare = async (locStart, locGoal, sTime) => { + const start = await locationModel.findOne({ koName: { $eq: locStart } }); + const goal = await locationModel + .findOne({ koName: { $eq: locGoal } }) + .clone(); let response = await axios.get( - `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${locations[locStart]}&goal=${locations[locGoal]}&options=traoptimal`, + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ + start.longitude + "," + start.latitude + }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, { headers: naverCloudApi } ); let fare = response.data.route.traoptimal[0].summary.taxiFare; From 163abbf7bd553868e8d2fc5c362fde0358857c9e Mon Sep 17 00:00:00 2001 From: ybmin Date: Sat, 9 Mar 2024 17:20:28 +0900 Subject: [PATCH 029/151] =?UTF-8?q?Add:=20=EC=9D=BC=EC=A3=BC=EC=9D=BC=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=EC=BA=90=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/fare.js | 7 ++++-- src/modules/stores/mongo.js | 2 +- src/schedules/updateMajorTaxiFare.js | 2 +- src/schedules/updateMinorTaxiFare.js | 5 ++-- src/services/fare.js | 36 +++++++++++++++------------- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 9dc1fe43..14b9d3f9 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -1,10 +1,13 @@ /** * 시간을 받아서 30분 단위로 변환해서 반환합니다. - * 00:00 ~ 23:59 -> 0 ~ 47 + * 요일 정보도 하나로 관리 + * @summary 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) * @param {Date} time 변환할 시간 */ const scaledTime = (time) => { - return time.getHours() * 2 + (time.getMinutes() >= 30 ? 1 : 0); + return ( + 48 * time.getDay() + time.getHours() * 2 + (time.getMinutes() >= 30 ? 1 : 0) + ); }; module.exports = { scaledTime }; diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 0000e74c..5f0d5f58 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -176,7 +176,7 @@ const taxiFareSchema = Schema( { start: { type: String, required: true }, // 출발지 goal: { type: String, required: true }, // 도착지 - time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리, 0 ~ 47 (0:00 ~ 23:30)) + time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) fare: { type: Number, default: false }, // 예상 택시 요금 }, { diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js index a21f305f..876220b7 100644 --- a/src/schedules/updateMajorTaxiFare.js +++ b/src/schedules/updateMajorTaxiFare.js @@ -1,7 +1,7 @@ const { scaledTime } = require("../modules/fare"); const { updateTaxiFare } = require("../services/fare"); -/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 캐싱합니다. */ +/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ module.exports = (app) => async () => { try { start = "카이스트 본원"; diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.js index 66b2b2e9..4099eaa7 100644 --- a/src/schedules/updateMinorTaxiFare.js +++ b/src/schedules/updateMinorTaxiFare.js @@ -1,12 +1,13 @@ const { updateTaxiFare } = require("../services/fare"); const { locationModel } = require("../modules/stores/mongo"); -/* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 캐싱합니다. */ +/* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 1주일 단위로 캐싱합니다. */ module.exports = (app) => async () => { try { const location = ( await locationModel.find({ isValid: { $ne: false } }, "koName") ).map((location) => location.koName); + const date = new Date(); for (let locStart in location) { for (let locGoal in location) { if (locStart === locGoal) continue; @@ -16,7 +17,7 @@ module.exports = (app) => async () => { ) continue; else { - await updateTaxiFare(locStart, locGoal, 0); //18:00시의 택시 요금이지만 db에는 0으로 저장됨 + await updateTaxiFare(locStart, locGoal, 48 * date.getDay()); // 18:00시의 택시 요금이지만 db에는 48*(요일) + 0으로 저장됨 } } } diff --git a/src/services/fare.js b/src/services/fare.js index 1944c912..31d999d3 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -13,8 +13,8 @@ const naverCloudApi = { /** Initialize database * 1. Erase all previous data * 2. Sets all taxi fare to 0 - * @summary 카이스트 본원 <-> 대전역의 경우 48개의 시간대에 대한 택시 요금을 0으로 설정합니다. - * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 time과 fare를 0으로 설정합니다. + * @summary 카이스트 본원 <-> 대전역의 경우 48 * 7개의 시간대에 대한 택시 요금을 0으로 설정합니다. + * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 fare를 0으로 설정합니다. time은 0으로 설정합니다. */ const initDatabase = async (req, res) => { try { @@ -29,12 +29,12 @@ const initDatabase = async (req, res) => { for (let gkey in location) { if (skey === gkey) continue; let tableFare = []; - // 카이스트 본원 <-> 대전역의 경우 48개의 시간대에 대한 택시 요금을 0으로 설정 + // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 0으로 설정 if ( (skey === "카이스트 본원" && gkey === "대전역") || (skey === "대전역" && gkey === "카이스트 본원") ) { - for (let i = 0; i < 48; i++) { + for (let i = 0; i < 336; i++) { tableFare.push({ start: skey, goal: gkey, @@ -43,14 +43,16 @@ const initDatabase = async (req, res) => { }); } } - // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 1개씩만 collection 지정 설정 + // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 else { - tableFare.push({ - start: skey, - goal: gkey, - time: 0, - fare: 0, - }); + for (let i = 0; i < 7; i++) { + tableFare.push({ + start: skey, + goal: gkey, + time: i * 48, + fare: 0, + }); + } } await taxiFareModel.insertMany(tableFare); } @@ -63,8 +65,8 @@ const initDatabase = async (req, res) => { /** * 주어진 start, goal, time에 대한 택시 요금을 반환합니다. - * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 매일 18:00시의 택시 요금을 반환합니다. - * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 존재하지 않을 경우에는 직접 호출합니다. + * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다. + * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다. * @param {Request} req - 파라미터로 start, goal, time을 받습니다. * - @param {String} start - 출발지 * - @param {String} goal - 도착지 @@ -102,7 +104,7 @@ const getTaxiFare = async (req, res) => { ) .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (taxiFare.fare === 0) { + if (!taxiFare || taxiFare.fare === 0) { let response = await axios.get( `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ start.longitude + "," + start.latitude @@ -132,7 +134,7 @@ const getTaxiFare = async (req, res) => { ) .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (taxiFare.fare === 0) { + if (!taxiFare || taxiFare.fare === 0) { let response = await axios.get( `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ start.longitude + "," + start.latitude @@ -151,12 +153,12 @@ const getTaxiFare = async (req, res) => { }; /** - * 주어진 start, goal, sTime에 대한 택시 요금을 업데이트합니다. + * 주어진 start, goal, sTime에 대한 단일 택시 요금을 업데이트합니다. * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. * @param {String} locStart - 출발지 string * @param {String} locGoal - 도착지 string - * @param {Date} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 47 (0:00 ~ 23:59)) + * @param {Date} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) */ const updateTaxiFare = async (locStart, locGoal, sTime) => { const start = await locationModel.findOne({ koName: { $eq: locStart } }); From a8cce549a89209da1972caef4a6b4b13ff16364e Mon Sep 17 00:00:00 2001 From: ybmin Date: Wed, 13 Mar 2024 15:16:53 +0900 Subject: [PATCH 030/151] Refactor: change fare location schema string to object id --- src/modules/stores/mongo.js | 5 +- src/routes/fare.js | 4 +- src/schedules/updateMajorTaxiFare.js | 8 +- src/schedules/updateMinorTaxiFare.js | 20 +-- src/services/fare.js | 183 +++++++++++++++++---------- 5 files changed, 124 insertions(+), 96 deletions(-) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 5f0d5f58..21f84d8f 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -174,8 +174,9 @@ const adminLogSchema = Schema({ const taxiFareSchema = Schema( { - start: { type: String, required: true }, // 출발지 - goal: { type: String, required: true }, // 도착지 + start: { type: Schema.Types.ObjectId, required: true }, // 출발지 + goal: { type: Schema.Types.ObjectId, required: true }, // 도착지 + isMajor: { type: Boolean, default: false }, // 카이스트 본원 <-> 대전역 경로 여부 time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) fare: { type: Number, default: false }, // 예상 택시 요금 }, diff --git a/src/routes/fare.js b/src/routes/fare.js index d03b560e..566d0979 100644 --- a/src/routes/fare.js +++ b/src/routes/fare.js @@ -12,8 +12,8 @@ router.get( "/getTaxiFare", async (req, res, next) => { req.locations = ( - await locationModel.find({ isValid: { $ne: false } }, "koName") - ).map((location) => location.koName); + await locationModel.find({ isValid: { $ne: false } }, "_id") + ).map((location) => location._id); next(); }, query("start").custom((value, { req }) => { diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js index 876220b7..abaff9d9 100644 --- a/src/schedules/updateMajorTaxiFare.js +++ b/src/schedules/updateMajorTaxiFare.js @@ -1,16 +1,14 @@ const { scaledTime } = require("../modules/fare"); +const logger = require("../modules/logger"); const { updateTaxiFare } = require("../services/fare"); /* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ module.exports = (app) => async () => { try { - start = "카이스트 본원"; - goal = "대전역"; time = new Date(); sTime = scaledTime(time); - await updateTaxiFare(start, goal, sTime); - await updateTaxiFare(goal, start, sTime); + await updateTaxiFare(sTime, true); } catch (err) { - console.log(err); + logger.error(err); } }; diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.js index 4099eaa7..b5201712 100644 --- a/src/schedules/updateMinorTaxiFare.js +++ b/src/schedules/updateMinorTaxiFare.js @@ -1,27 +1,11 @@ const { updateTaxiFare } = require("../services/fare"); -const { locationModel } = require("../modules/stores/mongo"); /* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 1주일 단위로 캐싱합니다. */ module.exports = (app) => async () => { try { - const location = ( - await locationModel.find({ isValid: { $ne: false } }, "koName") - ).map((location) => location.koName); const date = new Date(); - for (let locStart in location) { - for (let locGoal in location) { - if (locStart === locGoal) continue; - if ( - (locStart === "카이스트 본원" && locGoal === "대전역") || - (locStart === "대전역" && locGoal === "카이스트 본원") - ) - continue; - else { - await updateTaxiFare(locStart, locGoal, 48 * date.getDay()); // 18:00시의 택시 요금이지만 db에는 48*(요일) + 0으로 저장됨 - } - } - } + await updateTaxiFare(48 * date.getDay(), false); // 18:00시의 택시 요금이지만 db에는 48*(요일) + 0으로 저장됨 } catch (err) { - console.log(err); + logger.error(err); } }; diff --git a/src/services/fare.js b/src/services/fare.js index 31d999d3..28043b27 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -3,60 +3,95 @@ const axios = require("axios"); const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); const { scaledTime } = require("../modules/fare"); +const logger = require("../modules/logger"); // Naver Cloud Platform Maps Directions 5 API Keys const naverCloudApi = { "X-NCP-APIGW-API-KEY-ID": naverCloudApiId, "X-NCP-APIGW-API-KEY": naverCloudApiKey, }; +const naverCloudApiCall = + "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; /** Initialize database * 1. Erase all previous data * 2. Sets all taxi fare to 0 - * @summary 카이스트 본원 <-> 대전역의 경우 48 * 7개의 시간대에 대한 택시 요금을 0으로 설정합니다. - * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 fare를 0으로 설정합니다. time은 0으로 설정합니다. + * @summary 카이스트 본원 <-> 대전역의 경우 48 * 7개의 시간대에 대한 택시 요금을 init 시점의 비용으로 설정합니다. + * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 fare를 init 시점의 비용으로 설정합니다. time은 0으로 설정합니다. */ const initDatabase = async (req, res) => { try { // Remove all previous data await taxiFareModel.deleteMany({}); - const location = ( - await locationModel.find({ isValid: { $ne: false } }, "koName") - ).map((location) => location.koName); + const location = await locationModel + .find({ isValid: { $ne: false } }) + .toArray(); - for (let skey in location) { - for (let gkey in location) { - if (skey === gkey) continue; + location.map((start) => { + location.map(async (goal) => { + if (start._id === goal._id) return; let tableFare = []; - // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 0으로 설정 - if ( - (skey === "카이스트 본원" && gkey === "대전역") || - (skey === "대전역" && gkey === "카이스트 본원") + // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 일괄적으로 설정 + if (start.koName === "카이스트 본원" && goal.koName === "대전역") { + const fare = ( + await axios.get( + `${ + naverCloudApiCall + start.longitude + "," + start.latitude + }&goal=${ + goal.longitude + "," + goal.latitude + }&options=traoptimal`, + { headers: naverCloudApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(48 * 7)].map((_, i) => { + tableFare.push({ + start: start._id, + goal: goal._id, + time: i, + fare: fare, + isMajor: true, + }); + }); + } else if ( + start.koName === "대전역" && + goal.koName === "카이스트 본원" ) { - for (let i = 0; i < 336; i++) { + const fare = ( + await axios.get( + `${ + naverCloudApiCall + start.longitude + "," + start.latitude + }&goal=${ + goal.longitude + "," + goal.latitude + }&options=traoptimal`, + { headers: naverCloudApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(48 * 7)].map((_, i) => { tableFare.push({ - start: skey, - goal: gkey, + start: start._id, + goal: goal._id, time: i, - fare: 0, + fare: fare, + isMajor: true, }); - } + }); } // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 else { - for (let i = 0; i < 7; i++) { + [...Array(7)].map((_, i) => { tableFare.push({ - start: skey, - goal: gkey, + start: start, + goal: goal, time: i * 48, fare: 0, + isMajor: false, }); - } + }); } await taxiFareModel.insertMany(tableFare); - } - } + }); + }); res.state(200).json({ message: "TaxiFare Database initialized" }); } catch (err) { res.status(500).json({ error: "Failed with exception " + err.message }); @@ -68,20 +103,20 @@ const initDatabase = async (req, res) => { * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다. * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다. * @param {Request} req - 파라미터로 start, goal, time을 받습니다. - * - @param {String} start - 출발지 - * - @param {String} goal - 도착지 + * - @param {mongoose.Schema.Types.ObjectId} start - 출발지 + * - @param {mongoose.Schema.Types.ObjectId} goal - 도착지 * - @param {Date} time - 출발 시간 (ISO 8601) */ const getTaxiFare = async (req, res) => { try { - let start = await locationModel.findOne({ - koName: { $eq: req.query.start }, + const start = await locationModel.findOne({ + _id: { $eq: req.query.start }, }); - let goal = await locationModel - .findOne({ koName: { $eq: req.query.goal } }) + const goal = await locationModel + .findOne({ _id: { $eq: req.query.goal } }) .clone(); - let time = new Date(req.query.time); - let sTime = scaledTime(time); // Scaled Time. 0 ~ 47 (0:00 ~ 23:30) + const time = new Date(req.query.time); + const sTime = scaledTime(time); // Scaled Time // 카이스트 본원 <-> 대전역 if ( @@ -91,13 +126,14 @@ const getTaxiFare = async (req, res) => { let taxiFare = await taxiFareModel .findOne( { - start: start.koName, - goal: goal.koName, + start: start._id, + goal: goal._id, time: sTime, + isMajor: true, }, function (err, docs) { if (err) - console.log( + logger.error( "Error occured while finding TaxiFare documents: " + err.message ); } @@ -106,9 +142,9 @@ const getTaxiFare = async (req, res) => { //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare === 0) { let response = await axios.get( - `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ - start.longitude + "," + start.latitude - }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, + `${naverCloudApiCall + start.longitude + "," + start.latitude}&goal=${ + goal.longitude + "," + goal.latitude + }&options=traoptimal`, { headers: naverCloudApi } ); res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); @@ -121,13 +157,14 @@ const getTaxiFare = async (req, res) => { let taxiFare = await taxiFareModel .findOne( { - start: start.koName, - goal: goal.koName, + start: start._id, + goal: goal._id, time: 0, + isMajor: false, }, function (err, docs) { if (err) - console.log( + logger.error( "Error occured while finding TaxiFare documents: " + err.message ); } @@ -136,9 +173,9 @@ const getTaxiFare = async (req, res) => { //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare === 0) { let response = await axios.get( - `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ - start.longitude + "," + start.latitude - }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, + `${naverCloudApiCall + start.longitude + "," + start.latitude}&goal=${ + goal.longitude + "," + goal.latitude + }&options=traoptimal`, { headers: naverCloudApi } ); res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); @@ -147,7 +184,7 @@ const getTaxiFare = async (req, res) => { } } } catch (err) { - console.log(err); + logger.error("Failed with exception: " + err.message); res.status(500).json({ error: "Failed with exception: " + err.message }); } }; @@ -156,32 +193,40 @@ const getTaxiFare = async (req, res) => { * 주어진 start, goal, sTime에 대한 단일 택시 요금을 업데이트합니다. * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. - * @param {String} locStart - 출발지 string - * @param {String} locGoal - 도착지 string * @param {Date} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) + * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 여부 */ -const updateTaxiFare = async (locStart, locGoal, sTime) => { - const start = await locationModel.findOne({ koName: { $eq: locStart } }); - const goal = await locationModel - .findOne({ koName: { $eq: locGoal } }) - .clone(); - let response = await axios.get( - `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${ - start.longitude + "," + start.latitude - }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, - { headers: naverCloudApi } - ); - let fare = response.data.route.traoptimal[0].summary.taxiFare; - await taxiFareModel.updateOne( - { start: locStart, goal: locGoal, time: sTime }, - { fare: fare }, - function (err, docs) { - if (err) - console.log( - "Error occured while updating TaxiFare document: " + err.message - ); - } - ); +const updateTaxiFare = async (sTime, isMajor) => { + const prevFare = await taxiFareModel.findOne({ + time: sTime, + isMajor: isMajor, + }); + prevFare.map(async (item) => { + const start = await locationModel.findOne({ _id: item.start }); + const goal = await locationModel.findOne({ _id: item.goal }); + const fare = ( + await axios.get( + `${naverCloudApiCall + start.longitude + "," + start.latitude}&goal=${ + goal.longitude + "," + goal.latitude + }&options=traoptimal`, + { headers: naverCloudApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + await taxiFareModel.updateOne( + { start: item.start, goal: item.goal, time: sTime }, + { fare: fare }, + function (err, docs) { + if (err) + logger.error( + "Error occured while updating TaxiFare document: " + err.message + ); + } + ); + }); }; -module.exports = { initDatabase, getTaxiFare, updateTaxiFare }; +module.exports = { + initDatabase, + getTaxiFare, + updateTaxiFare, +}; From 1120f429183524a026ab510377a36c6f74d89934 Mon Sep 17 00:00:00 2001 From: ybmin Date: Wed, 13 Mar 2024 21:56:18 +0900 Subject: [PATCH 031/151] Refactor: express-validator to ajv & swagger docs --- loadenv.js | 2 +- src/routes/docs/fare.js | 78 +++++++++++++++++++++++++++ src/routes/docs/schemas/fareSchema.js | 24 +++++++++ src/routes/fare.js | 32 ++--------- src/services/fare.js | 55 +++++++++++-------- 5 files changed, 139 insertions(+), 52 deletions(-) create mode 100644 src/routes/docs/fare.js create mode 100644 src/routes/docs/schemas/fareSchema.js diff --git a/loadenv.js b/loadenv.js index 411e10f9..9269f3e1 100644 --- a/loadenv.js +++ b/loadenv.js @@ -1,5 +1,5 @@ // 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옴 -require("dotenv").config({ path: `./.env.${process.env.NODE_ENV}` }); +require("dotenv").config({ path: `./.env.development` }); module.exports = { nodeEnv: process.env.NODE_ENV, // required ("production" or "development" or "test") diff --git a/src/routes/docs/fare.js b/src/routes/docs/fare.js new file mode 100644 index 00000000..59da6d97 --- /dev/null +++ b/src/routes/docs/fare.js @@ -0,0 +1,78 @@ +const { response } = require("express"); +const { objectIdPattern } = require("./utils"); + +const tag = "fare"; +const apiPrefix = "/fare"; + +const fareDocs = {}; +fareDocs[`${apiPrefix}/init`] = { + post: { + tags: [tag], + summary: "택시 요금 db 초기화", + }, + response: { + 200: { + description: "TaxiFare Database initialized", + content: { + "text/plain": { + schema: { + type: "string", + example: "TaxiFare Database initialized", + }, + }, + }, + }, + 500: { + description: "TaxiFare Database failed", + content: { + "text/html": { + example: "fare/init : TaxiFare Database failed", + }, + }, + }, + }, +}; + +fareDocs[`${apiPrefix}/getTaxiFare`] = { + get: { + tags: [tag], + summary: "예상 택시 요금 반환", + description: + "start, goal, time에 따라 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다.
카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다.", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + start: { type: "string", pattern: objectIdPattern }, + goal: { type: "string", pattern: objectIdPattern }, + time: { type: "string", format: "date-time" }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: "예상 택시 요금 반환 성공", + content: { + "text/plain": { + schema: { + type: "number", + example: 10000, + }, + }, + }, + }, + 500: { + description: "fare/getTaxiFare: Failed to load taxi fare", + content: { + "text/html": { + example: "fare/getTaxiFare: Failed to load taxi fare", + }, + }, + }, + }, + }, +}; diff --git a/src/routes/docs/schemas/fareSchema.js b/src/routes/docs/schemas/fareSchema.js new file mode 100644 index 00000000..ff887663 --- /dev/null +++ b/src/routes/docs/schemas/fareSchema.js @@ -0,0 +1,24 @@ +const { objectIdPattern } = require("../utils"); + +const fareSchema = { + getTaxiFare: { + type: "object", + required: ["start", "goal", "time"], + properties: { + start: { + type: "string", + format: objectIdPattern, + }, + goal: { + type: "string", + format: objectIdPattern, + }, + time: { + type: "string", + format: "date-time", + }, + }, + errorMessage: "validation: bad request", + }, +}; +module.exports = fareSchema; diff --git a/src/routes/fare.js b/src/routes/fare.js index 566d0979..7441d0ee 100644 --- a/src/routes/fare.js +++ b/src/routes/fare.js @@ -1,36 +1,12 @@ const express = require("express"); -const { query } = require("express-validator"); +const fareSchema = require("./docs/schemas/fareSchema"); +const { validateQuery } = require("../middlewares/ajv"); const router = express.Router(); -const validator = require("../middlewares/validator"); const { getTaxiFare, initDatabase } = require("../services/fare"); -const { locationModel } = require("../modules/stores/mongo"); -router.get("/init", initDatabase); +router.post("/init", initDatabase); -router.get( - "/getTaxiFare", - async (req, res, next) => { - req.locations = ( - await locationModel.find({ isValid: { $ne: false } }, "_id") - ).map((location) => location._id); - next(); - }, - query("start").custom((value, { req }) => { - if (!req.locations.includes(value)) { - throw new Error("Invalid start location"); - } - return true; - }), - query("goal").custom((value, { req }) => { - if (!req.locations.includes(value)) { - throw new Error("Invalid goal location"); - } - return true; - }), - query("time").isISO8601(), - validator, - getTaxiFare -); +router.get("/getTaxiFare", validateQuery(fareSchema.getTaxiFare), getTaxiFare); module.exports = router; diff --git a/src/services/fare.js b/src/services/fare.js index 28043b27..3526465b 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -92,9 +92,9 @@ const initDatabase = async (req, res) => { await taxiFareModel.insertMany(tableFare); }); }); - res.state(200).json({ message: "TaxiFare Database initialized" }); + res.status(200).send("TaxiFare Database initialized"); } catch (err) { - res.status(500).json({ error: "Failed with exception " + err.message }); + res.status(500).json({ error: "fare/init: TaxiFare Database failed" }); } }; @@ -115,15 +115,18 @@ const getTaxiFare = async (req, res) => { const goal = await locationModel .findOne({ _id: { $eq: req.query.goal } }) .clone(); - const time = new Date(req.query.time); - const sTime = scaledTime(time); // Scaled Time + const sTime = scaledTime(new Date(req.query.time)); + + if (!start || !goal) { + res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); + } // 카이스트 본원 <-> 대전역 if ( (start.koName === "카이스트 본원" && goal.koName === "대전역") || (start.koName === "대전역" && goal.koName === "카이스트 본원") ) { - let taxiFare = await taxiFareModel + const taxiFare = await taxiFareModel .findOne( { start: start._id, @@ -141,20 +144,22 @@ const getTaxiFare = async (req, res) => { .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare === 0) { - let response = await axios.get( - `${naverCloudApiCall + start.longitude + "," + start.latitude}&goal=${ - goal.longitude + "," + goal.latitude - }&options=traoptimal`, - { headers: naverCloudApi } - ); - res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); + const fare = ( + await axios.get( + `${ + naverCloudApiCall + start.longitude + "," + start.latitude + }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, + { headers: naverCloudApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + res.status(200).send(fare); } else { - res.json({ fare: taxiFare.fare }); + res.status(200).send(taxiFare.fare); } } // 카이스트 본원 <-> 대전역이 아닌 경우 else { - let taxiFare = await taxiFareModel + const taxiFare = await taxiFareModel .findOne( { start: start._id, @@ -172,20 +177,24 @@ const getTaxiFare = async (req, res) => { .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare === 0) { - let response = await axios.get( - `${naverCloudApiCall + start.longitude + "," + start.latitude}&goal=${ - goal.longitude + "," + goal.latitude - }&options=traoptimal`, - { headers: naverCloudApi } - ); - res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare }); + const fare = ( + await axios.get( + `${ + naverCloudApiCall + start.longitude + "," + start.latitude + }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, + { headers: naverCloudApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + res.status(200).send(fare); } else { - res.json({ fare: taxiFare.fare }); + res.status(200).send(taxiFare.fare); } } } catch (err) { logger.error("Failed with exception: " + err.message); - res.status(500).json({ error: "Failed with exception: " + err.message }); + res + .status(500) + .json({ error: "fare/getTaxiFare: Failed to load taxi fare" }); } }; From 2ac458a943136dd444251f067bf65733cb75895d Mon Sep 17 00:00:00 2001 From: ybmin Date: Wed, 13 Mar 2024 22:50:01 +0900 Subject: [PATCH 032/151] Refactor: start, goal to from, to --- src/modules/stores/mongo.js | 4 +- src/routes/docs/fare.js | 4 +- src/routes/docs/schemas/fareSchema.js | 6 +- src/services/fare.js | 91 +++++++++++++-------------- 4 files changed, 49 insertions(+), 56 deletions(-) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 21f84d8f..2f9ccbbd 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -174,8 +174,8 @@ const adminLogSchema = Schema({ const taxiFareSchema = Schema( { - start: { type: Schema.Types.ObjectId, required: true }, // 출발지 - goal: { type: Schema.Types.ObjectId, required: true }, // 도착지 + from: { type: Schema.Types.ObjectId, required: true }, // 출발지 + to: { type: Schema.Types.ObjectId, required: true }, // 도착지 isMajor: { type: Boolean, default: false }, // 카이스트 본원 <-> 대전역 경로 여부 time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) fare: { type: Number, default: false }, // 예상 택시 요금 diff --git a/src/routes/docs/fare.js b/src/routes/docs/fare.js index 59da6d97..bda96260 100644 --- a/src/routes/docs/fare.js +++ b/src/routes/docs/fare.js @@ -45,8 +45,8 @@ fareDocs[`${apiPrefix}/getTaxiFare`] = { schema: { type: "object", properties: { - start: { type: "string", pattern: objectIdPattern }, - goal: { type: "string", pattern: objectIdPattern }, + from: { type: "string", pattern: objectIdPattern }, + to: { type: "string", pattern: objectIdPattern }, time: { type: "string", format: "date-time" }, }, }, diff --git a/src/routes/docs/schemas/fareSchema.js b/src/routes/docs/schemas/fareSchema.js index ff887663..b725c42a 100644 --- a/src/routes/docs/schemas/fareSchema.js +++ b/src/routes/docs/schemas/fareSchema.js @@ -3,13 +3,13 @@ const { objectIdPattern } = require("../utils"); const fareSchema = { getTaxiFare: { type: "object", - required: ["start", "goal", "time"], + required: ["from", "to", "time"], properties: { - start: { + from: { type: "string", format: objectIdPattern, }, - goal: { + to: { type: "string", format: objectIdPattern, }, diff --git a/src/services/fare.js b/src/services/fare.js index 3526465b..9db0d9ac 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -28,49 +28,42 @@ const initDatabase = async (req, res) => { .find({ isValid: { $ne: false } }) .toArray(); - location.map((start) => { - location.map(async (goal) => { - if (start._id === goal._id) return; + location.map((from) => { + location.map(async (to) => { + if (from._id === to._id) return; let tableFare = []; // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 일괄적으로 설정 - if (start.koName === "카이스트 본원" && goal.koName === "대전역") { + if (from.koName === "카이스트 본원" && to.koName === "대전역") { const fare = ( await axios.get( `${ - naverCloudApiCall + start.longitude + "," + start.latitude - }&goal=${ - goal.longitude + "," + goal.latitude - }&options=traoptimal`, + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, { headers: naverCloudApi } ) ).data.route.traoptimal[0].summary.taxiFare; [...Array(48 * 7)].map((_, i) => { tableFare.push({ - start: start._id, - goal: goal._id, + from: from._id, + to: to._id, time: i, fare: fare, isMajor: true, }); }); - } else if ( - start.koName === "대전역" && - goal.koName === "카이스트 본원" - ) { + } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { const fare = ( await axios.get( `${ - naverCloudApiCall + start.longitude + "," + start.latitude - }&goal=${ - goal.longitude + "," + goal.latitude - }&options=traoptimal`, + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, { headers: naverCloudApi } ) ).data.route.traoptimal[0].summary.taxiFare; [...Array(48 * 7)].map((_, i) => { tableFare.push({ - start: start._id, - goal: goal._id, + from: from._id, + to: to._id, time: i, fare: fare, isMajor: true, @@ -81,8 +74,8 @@ const initDatabase = async (req, res) => { else { [...Array(7)].map((_, i) => { tableFare.push({ - start: start, - goal: goal, + from: from, + to: to, time: i * 48, fare: 0, isMajor: false, @@ -99,38 +92,38 @@ const initDatabase = async (req, res) => { }; /** - * 주어진 start, goal, time에 대한 택시 요금을 반환합니다. + * 주어진 from, to, time에 대한 택시 요금을 반환합니다. * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다. * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다. - * @param {Request} req - 파라미터로 start, goal, time을 받습니다. - * - @param {mongoose.Schema.Types.ObjectId} start - 출발지 - * - @param {mongoose.Schema.Types.ObjectId} goal - 도착지 + * @param {Request} req - 파라미터로 from, to, time을 받습니다. + * - @param {mongoose.Schema.Types.ObjectId} from - 출발지 + * - @param {mongoose.Schema.Types.ObjectId} to - 도착지 * - @param {Date} time - 출발 시간 (ISO 8601) */ const getTaxiFare = async (req, res) => { try { - const start = await locationModel.findOne({ - _id: { $eq: req.query.start }, + const from = await locationModel.findOne({ + _id: { $eq: req.query.from }, }); - const goal = await locationModel - .findOne({ _id: { $eq: req.query.goal } }) + const to = await locationModel + .findOne({ _id: { $eq: req.query.to } }) .clone(); const sTime = scaledTime(new Date(req.query.time)); - if (!start || !goal) { + if (!from || !to) { res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); } // 카이스트 본원 <-> 대전역 if ( - (start.koName === "카이스트 본원" && goal.koName === "대전역") || - (start.koName === "대전역" && goal.koName === "카이스트 본원") + (from.koName === "카이스트 본원" && to.koName === "대전역") || + (from.koName === "대전역" && to.koName === "카이스트 본원") ) { const taxiFare = await taxiFareModel .findOne( { - start: start._id, - goal: goal._id, + from: from._id, + to: to._id, time: sTime, isMajor: true, }, @@ -146,9 +139,9 @@ const getTaxiFare = async (req, res) => { if (!taxiFare || taxiFare.fare === 0) { const fare = ( await axios.get( - `${ - naverCloudApiCall + start.longitude + "," + start.latitude - }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, + `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, { headers: naverCloudApi } ) ).data.route.traoptimal[0].summary.taxiFare; @@ -162,8 +155,8 @@ const getTaxiFare = async (req, res) => { const taxiFare = await taxiFareModel .findOne( { - start: start._id, - goal: goal._id, + from: from._id, + to: to._id, time: 0, isMajor: false, }, @@ -179,9 +172,9 @@ const getTaxiFare = async (req, res) => { if (!taxiFare || taxiFare.fare === 0) { const fare = ( await axios.get( - `${ - naverCloudApiCall + start.longitude + "," + start.latitude - }&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`, + `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, { headers: naverCloudApi } ) ).data.route.traoptimal[0].summary.taxiFare; @@ -199,7 +192,7 @@ const getTaxiFare = async (req, res) => { }; /** - * 주어진 start, goal, sTime에 대한 단일 택시 요금을 업데이트합니다. + * 주어진 from, to, sTime에 대한 단일 택시 요금을 업데이트합니다. * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. * @param {Date} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) @@ -211,18 +204,18 @@ const updateTaxiFare = async (sTime, isMajor) => { isMajor: isMajor, }); prevFare.map(async (item) => { - const start = await locationModel.findOne({ _id: item.start }); - const goal = await locationModel.findOne({ _id: item.goal }); + const from = await locationModel.findOne({ _id: item.from }); + const to = await locationModel.findOne({ _id: item.to }); const fare = ( await axios.get( - `${naverCloudApiCall + start.longitude + "," + start.latitude}&goal=${ - goal.longitude + "," + goal.latitude + `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude }&options=traoptimal`, { headers: naverCloudApi } ) ).data.route.traoptimal[0].summary.taxiFare; await taxiFareModel.updateOne( - { start: item.start, goal: item.goal, time: sTime }, + { from: item.from, to: item.to, time: sTime }, { fare: fare }, function (err, docs) { if (err) From 840b99efbe86dd0f4dfa027f785dc61f28c91261 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 19 Mar 2024 23:10:42 +0900 Subject: [PATCH 033/151] Fix: init error case --- src/services/fare.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/services/fare.js b/src/services/fare.js index 9db0d9ac..f7ed765f 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -24,9 +24,7 @@ const initDatabase = async (req, res) => { // Remove all previous data await taxiFareModel.deleteMany({}); - const location = await locationModel - .find({ isValid: { $ne: false } }) - .toArray(); + const location = await locationModel.find({ isValid: { $eq: true } }); location.map((from) => { location.map(async (to) => { @@ -72,12 +70,21 @@ const initDatabase = async (req, res) => { } // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 else { + const fare = ( + await axios.get( + `${ + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverCloudApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + setTimeout(() => {}, 100); [...Array(7)].map((_, i) => { tableFare.push({ from: from, to: to, time: i * 48, - fare: 0, + fare: fare, isMajor: false, }); }); @@ -111,7 +118,9 @@ const getTaxiFare = async (req, res) => { const sTime = scaledTime(new Date(req.query.time)); if (!from || !to) { + console.log("asds"); res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); + return; } // 카이스트 본원 <-> 대전역 @@ -119,13 +128,13 @@ const getTaxiFare = async (req, res) => { (from.koName === "카이스트 본원" && to.koName === "대전역") || (from.koName === "대전역" && to.koName === "카이스트 본원") ) { + console.log("asds"); const taxiFare = await taxiFareModel .findOne( { from: from._id, to: to._id, time: sTime, - isMajor: true, }, function (err, docs) { if (err) @@ -145,9 +154,9 @@ const getTaxiFare = async (req, res) => { { headers: naverCloudApi } ) ).data.route.traoptimal[0].summary.taxiFare; - res.status(200).send(fare); + res.state(200).send(fare); } else { - res.status(200).send(taxiFare.fare); + res.state(200).send(taxiFare.fare); } } // 카이스트 본원 <-> 대전역이 아닌 경우 @@ -158,7 +167,6 @@ const getTaxiFare = async (req, res) => { from: from._id, to: to._id, time: 0, - isMajor: false, }, function (err, docs) { if (err) @@ -168,6 +176,7 @@ const getTaxiFare = async (req, res) => { } ) .clone(); + console.log(taxiFare.fare); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare === 0) { const fare = ( @@ -178,13 +187,13 @@ const getTaxiFare = async (req, res) => { { headers: naverCloudApi } ) ).data.route.traoptimal[0].summary.taxiFare; - res.status(200).send(fare); + res.send(fare); } else { - res.status(200).send(taxiFare.fare); + res.send(taxiFare.fare); } } } catch (err) { - logger.error("Failed with exception: " + err.message); + logger.error(err.message); res .status(500) .json({ error: "fare/getTaxiFare: Failed to load taxi fare" }); @@ -199,11 +208,11 @@ const getTaxiFare = async (req, res) => { * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 여부 */ const updateTaxiFare = async (sTime, isMajor) => { - const prevFare = await taxiFareModel.findOne({ + const prevFares = await taxiFareModel.find({ time: sTime, isMajor: isMajor, }); - prevFare.map(async (item) => { + prevFares.map(async (item) => { const from = await locationModel.findOne({ _id: item.from }); const to = await locationModel.findOne({ _id: item.to }); const fare = ( From 86f8b4af2d34fb35df1bfb51692de95eb55b49b8 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 20 Mar 2024 23:18:16 +0900 Subject: [PATCH 034/151] Remove: all references to lottery module from outside lottery module --- src/services/notifications.js | 12 +-- src/services/rooms.js | 153 +++++++++++++++++----------------- src/services/users.js | 24 +++--- 3 files changed, 95 insertions(+), 94 deletions(-) diff --git a/src/services/notifications.js b/src/services/notifications.js index 7134fab5..7a0b810e 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -5,7 +5,7 @@ const logger = require("@/modules/logger"); const { registerDeviceToken, validateDeviceToken } = require("@/modules/fcm"); // 이벤트 코드입니다. -const { contracts } = require("../lottery"); +// const { contracts } = require("../lottery"); const registerDeviceTokenHandler = async (req, res) => { try { @@ -108,11 +108,11 @@ const editOptionsHandler = async (req, res) => { } // 이벤트 코드입니다. - await contracts?.completeAdPushAgreementQuest( - req.userOid, - req.timestamp, - options.advertisement - ); + // await contracts?.completeAdPushAgreementQuest( + // req.userOid, + // req.timestamp, + // options.advertisement + // ); res.status(200).json(updatedNotificationOptions); } catch (err) { diff --git a/src/services/rooms.js b/src/services/rooms.js index 1a5a444b..f2dd1cbf 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -15,12 +15,12 @@ const { } = require("@/modules/slackNotification"); // 이벤트 코드입니다. -const { eventConfig } = require("../../loadenv"); -const eventPeriod = eventConfig && { - startAt: new Date(eventConfig.period.startAt), - endAt: new Date(eventConfig.period.endAt), -}; -const { contracts } = require("../lottery"); +// const { eventConfig } = require("../../loadenv"); +// const eventPeriod = eventConfig && { +// startAt: new Date(eventConfig.period.startAt), +// endAt: new Date(eventConfig.period.endAt), +// }; +// const { contracts } = require("../lottery"); const createHandler = async (req, res) => { const { name, from, to, time, maxPartLength } = req.body; @@ -102,7 +102,7 @@ const createHandler = async (req, res) => { const roomObjectFormated = formatSettlement(roomObject); // 이벤트 코드입니다. - await contracts?.completeFirstRoomCreationQuest(req.userOid, req.timestamp); + // await contracts?.completeFirstRoomCreationQuest(req.userOid, req.timestamp); return res.send(roomObjectFormated); } catch (err) { @@ -172,56 +172,57 @@ const createTestHandler = async (req, res) => { try { // 이벤트 코드입니다. - if ( - !eventPeriod || - req.timestamp >= eventPeriod.endAt || - req.timestamp < eventPeriod.startAt - ) - return res.json({ result: true }); - - const countRecentlyMadeRooms = await roomModel.countDocuments({ - madeat: { $gte: new Date(req.timestamp - 86400000) }, // 밀리초 단위로 24시간을 나타냅니다. - "part.0.user": req.userOid, // 방 최초 생성자를 저장하는 필드가 없으므로, 첫 번째 참여자를 생성자로 간주합니다. - }); - if (!countRecentlyMadeRooms && countRecentlyMadeRooms !== 0) - return res - .status(500) - .json({ error: "Rooms/create/test : internal server error" }); - - const dateTime = new Date(time); - const candidateRooms = await roomModel - .find( - { - time: { - $gte: new Date(dateTime.getTime() - 43200000), - $lte: new Date(dateTime.getTime() + 43200000), - }, - part: { $elemMatch: { user: req.userOid } }, - }, - "from to time maxPartLength" - ) - .limit(2) - .lean(); - if (!candidateRooms) - return res - .status(500) - .json({ error: "Rooms/create/test : internal server error" }); - - const isAbusing = checkIsAbusing( - req.body, - countRecentlyMadeRooms, - candidateRooms - ); - if (isAbusing) { - const user = await userModel.findById(req.userOid).lean(); - notifyRoomCreationAbuseToReportChannel( - req.userOid, - user?.nickname ?? req.userOid, - req.body - ); - } + // if ( + // !eventPeriod || + // req.timestamp >= eventPeriod.endAt || + // req.timestamp < eventPeriod.startAt + // ) + // return res.json({ result: true }); + + // const countRecentlyMadeRooms = await roomModel.countDocuments({ + // madeat: { $gte: new Date(req.timestamp - 86400000) }, // 밀리초 단위로 24시간을 나타냅니다. + // "part.0.user": req.userOid, // 방 최초 생성자를 저장하는 필드가 없으므로, 첫 번째 참여자를 생성자로 간주합니다. + // }); + // if (!countRecentlyMadeRooms && countRecentlyMadeRooms !== 0) + // return res + // .status(500) + // .json({ error: "Rooms/create/test : internal server error" }); + + // const dateTime = new Date(time); + // const candidateRooms = await roomModel + // .find( + // { + // time: { + // $gte: new Date(dateTime.getTime() - 43200000), + // $lte: new Date(dateTime.getTime() + 43200000), + // }, + // part: { $elemMatch: { user: req.userOid } }, + // }, + // "from to time maxPartLength" + // ) + // .limit(2) + // .lean(); + // if (!candidateRooms) + // return res + // .status(500) + // .json({ error: "Rooms/create/test : internal server error" }); + + // const isAbusing = checkIsAbusing( + // req.body, + // countRecentlyMadeRooms, + // candidateRooms + // ); + // if (isAbusing) { + // const user = await userModel.findById(req.userOid).lean(); + // notifyRoomCreationAbuseToReportChannel( + // req.userOid, + // user?.nickname ?? req.userOid, + // req.body + // ); + // } - return res.json({ result: !isAbusing }); + // return res.json({ result: !isAbusing }); + return res.json({ result: true }); } catch (err) { logger.error(err); res.status(500).json({ @@ -622,16 +623,16 @@ const commitPaymentHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completePayingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); + // await contracts?.completePayingQuest( + // req.userOid, + // req.timestamp, + // roomObject + // ); + // await contracts?.completePayingAndSendingQuest( + // req.userOid, + // req.timestamp, + // roomObject + // ); // 수정한 방 정보를 반환합니다. res.send(formatSettlement(roomObject, { isOver: true })); @@ -700,16 +701,16 @@ const settlementHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completeSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); + // await contracts?.completeSendingQuest( + // req.userOid, + // req.timestamp, + // roomObject + // ); + // await contracts?.completePayingAndSendingQuest( + // req.userOid, + // req.timestamp, + // roomObject + // ); // 수정한 방 정보를 반환합니다. res.send(formatSettlement(roomObject, { isOver: true })); diff --git a/src/services/users.js b/src/services/users.js index d3adde6f..10008c0a 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,14 +1,14 @@ const { userModel } = require("@/modules/stores/mongo"); const logger = require("@/modules/logger"); const aws = require("@/modules/stores/aws"); - -// 이벤트 코드입니다. -const { contracts } = require("../lottery"); const { generateNickname, generateProfileImageUrl, } = require("@/modules/modifyProfile"); +// 이벤트 코드입니다. +// const { contracts } = require("../lottery"); + const agreeOnTermsOfServiceHandler = async (req, res) => { try { let user = await userModel.findOne({ id: req.userId }); @@ -52,10 +52,10 @@ const editNicknameHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. - await contracts?.completeNicknameChangingQuest( - req.userOid, - req.timestamp - ); + // await contracts?.completeNicknameChangingQuest( + // req.userOid, + // req.timestamp + // ); res .status(200) @@ -79,11 +79,11 @@ const editAccountHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. - await contracts?.completeAccountChangingQuest( - req.userOid, - req.timestamp, - newAccount - ); + // await contracts?.completeAccountChangingQuest( + // req.userOid, + // req.timestamp, + // newAccount + // ); res.status(200).send("Users/editAccount : edit user account successful"); } else { From 2a32d961eb214a3ec6c4e0364ced6cf1daec15b6 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 20 Mar 2024 23:42:13 +0900 Subject: [PATCH 035/151] Fix: runtime errors --- scripts/profileImageUrlUpdater.js | 2 +- src/middlewares/zod.js | 2 +- src/modules/email.js | 4 ++-- src/modules/socket.js | 14 +++++++------- src/routes/admin.js | 6 +++--- src/routes/auth.js | 2 +- src/routes/chats.js | 6 +++--- src/routes/docs.js | 2 +- src/routes/docs/auth.replace.js | 2 +- src/routes/docs/chats.js | 2 +- src/routes/docs/logininfo.js | 2 +- src/routes/docs/reports.js | 2 +- src/routes/docs/rooms.js | 2 +- src/routes/docs/schemas/reportsSchema.js | 2 +- src/routes/docs/schemas/roomsSchema.js | 2 +- src/routes/docs/swaggerDocs.js | 2 +- src/routes/docs/utils.js | 2 +- src/routes/notifications.js | 4 ++-- src/routes/reports.js | 4 ++-- src/routes/rooms.js | 6 +++--- src/routes/users.js | 6 +++--- src/schedules/notifyAfterArrival.js | 2 +- src/schedules/notifyBeforeDepart.js | 2 +- src/services/auth.js | 4 ++-- src/services/auth.mobile.js | 4 ++-- src/services/auth.replace.js | 6 +++--- src/services/chats.js | 4 ++-- src/services/locations.js | 2 +- src/services/logininfo.js | 2 +- src/services/notifications.js | 4 ++-- src/services/reports.js | 4 ++-- src/services/rooms.js | 6 +++--- src/services/users.js | 6 +++--- test/utils.js | 2 +- 34 files changed, 62 insertions(+), 62 deletions(-) diff --git a/scripts/profileImageUrlUpdater.js b/scripts/profileImageUrlUpdater.js index 78ebe778..2b35bf1a 100644 --- a/scripts/profileImageUrlUpdater.js +++ b/scripts/profileImageUrlUpdater.js @@ -2,7 +2,7 @@ // https://github.com/sparcs-kaist/taxi-back/issues/173 const { MongoClient } = require("mongodb"); -const { mongo: mongoUrl, aws: awsEnv } = require("../loadenv"); +const { mongo: mongoUrl, aws: awsEnv } = require("../loadenv"); // FIXME: 올바른 경로로 수정해야 합니다. const time = Date.now(); diff --git a/src/middlewares/zod.js b/src/middlewares/zod.js index 63f5668a..b8660634 100644 --- a/src/middlewares/zod.js +++ b/src/middlewares/zod.js @@ -1,4 +1,4 @@ -const logger = require("../modules/logger"); +const logger = require("@/modules/logger").default; const parseZodErrors = (statusCode, errors, res) => { const error_message = errors; diff --git a/src/modules/email.js b/src/modules/email.js index 05bdfe3e..d6a20285 100644 --- a/src/modules/email.js +++ b/src/modules/email.js @@ -1,6 +1,6 @@ const nodemailer = require("nodemailer"); -const logger = require("./logger"); -const { nodeEnv } = require("../../loadenv"); +const logger = require("@/modules/logger").default; +const { nodeEnv } = require("@/loadenv"); /** * production 환경에서 메일을 전송하기 위해 사용되는 agent입니다. diff --git a/src/modules/socket.js b/src/modules/socket.js index 7741e597..c4de7f56 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -1,14 +1,14 @@ const { Server } = require("socket.io"); -const sessionMiddleware = require("@/middlewares/session"); -const logger = require("./logger"); -const { getLoginInfo } = require("./auths/login"); -const { roomModel, userModel, chatModel } = require("./stores/mongo"); -const { getS3Url } = require("./stores/aws"); -const { getTokensOfUsers, sendMessageByTokens } = require("./fcm"); +const sessionMiddleware = require("@/middlewares/session").default; +const logger = require("@/modules/logger").default; +const { getLoginInfo } = require("@/modules/auths/login"); +const { roomModel, userModel, chatModel } = require("@/modules/stores/mongo"); +const { getS3Url } = require("@/modules/stores/aws"); +const { getTokensOfUsers, sendMessageByTokens } = require("@/modules/fcm"); const { corsWhiteList } = require("@/loadenv"); -const { chatPopulateOption } = require("./populates/chats"); +const { chatPopulateOption } = require("@/modules/populates/chats"); /** * emitChatEvent의 필수 파라미터가 주어지지 않은 경우 발생하는 예외를 정의하는 클래스입니다. diff --git a/src/routes/admin.js b/src/routes/admin.js index d495497c..81cf20a3 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -18,8 +18,8 @@ const { buildResource } = require("@/modules/adminResource"); const router = express.Router(); // Requires admin property of the user to enter admin page. -router.use(require("@/middlewares/authAdmin")); -router.use(require("@/middlewares/auth")); +router.use(require("@/middlewares/authAdmin").default); +router.use(require("@/middlewares/auth").default); // Registration of the mongoose adapter AdminJS.registerAdapter(AdminJSMongoose); @@ -36,7 +36,7 @@ const resources = [ notificationOptionModel, ] .map(buildResource()) - .concat(require("../lottery").resources); + .concat(/*require("@/lottery").resources*/ []); // Create router for admin page const adminJS = new AdminJS({ resources }); diff --git a/src/routes/auth.js b/src/routes/auth.js index 9ce57404..534814f6 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); const { body, query } = require("express-validator"); -const validator = require("@/middlewares/validator"); +const validator = require("@/middlewares/validator").default; const authHandlers = require("@/services/auth"); const authReplaceHandlers = require("@/services/auth.replace"); diff --git a/src/routes/chats.js b/src/routes/chats.js index f27ace38..8af77944 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -1,13 +1,13 @@ const express = require("express"); const { body } = require("express-validator"); -const validator = require("@/middlewares/validator"); -const patterns = require("@/modules/patterns"); +const validator = require("@/middlewares/validator").default; +const patterns = require("@/modules/patterns").default; const router = express.Router(); const chatsHandlers = require("@/services/chats"); // 라우터 접근 시 로그인 필요 -router.use(require("@/middlewares/auth")); +router.use(require("@/middlewares/auth").default); /** * 가장 최근에 도착한 60개의 채팅을 가져옵니다. diff --git a/src/routes/docs.js b/src/routes/docs.js index 97a1b288..0a571d8e 100644 --- a/src/routes/docs.js +++ b/src/routes/docs.js @@ -1,6 +1,6 @@ const express = require("express"); const swaggerUi = require("swagger-ui-express"); -const swaggerDocs = require("./docs/swaggerDocs.js"); +const swaggerDocs = require("./docs/swaggerDocs"); const router = express.Router(); router.use(swaggerUi.serve); diff --git a/src/routes/docs/auth.replace.js b/src/routes/docs/auth.replace.js index 8246836b..be8b8174 100644 --- a/src/routes/docs/auth.replace.js +++ b/src/routes/docs/auth.replace.js @@ -1,4 +1,4 @@ -const loginReplacePage = require("../../views/loginReplacePage"); +const loginReplacePage = require("../../views/loginReplacePage").default; const tag = "auth"; const apiPrefix = "/auth(dev)"; diff --git a/src/routes/docs/chats.js b/src/routes/docs/chats.js index 0aaa1c6d..81834042 100644 --- a/src/routes/docs/chats.js +++ b/src/routes/docs/chats.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("../../modules/patterns").default; const tag = "chats"; const apiPrefix = "/chats"; diff --git a/src/routes/docs/logininfo.js b/src/routes/docs/logininfo.js index 7c447760..caec1f96 100644 --- a/src/routes/docs/logininfo.js +++ b/src/routes/docs/logininfo.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("../../modules/patterns").default; const tag = "logininfo"; const apiPrefix = "/logininfo"; diff --git a/src/routes/docs/reports.js b/src/routes/docs/reports.js index a11933ee..3acf99da 100644 --- a/src/routes/docs/reports.js +++ b/src/routes/docs/reports.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("../../modules/patterns").default; const tag = "reports"; const apiPrefix = "/reports"; diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 710bf649..a481bbc1 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -1,4 +1,4 @@ -const { objectId, room } = require("../../modules/patterns"); +const { objectId, room } = require("../../modules/patterns").default; const tag = "rooms"; const apiPrefix = "/rooms"; diff --git a/src/routes/docs/schemas/reportsSchema.js b/src/routes/docs/schemas/reportsSchema.js index d208dbb7..0e4c43b9 100644 --- a/src/routes/docs/schemas/reportsSchema.js +++ b/src/routes/docs/schemas/reportsSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../utils"); -const { objectId } = require("../../../modules/patterns"); +const { objectId } = require("../../../modules/patterns").default; const reportsZod = { createHandler: z diff --git a/src/routes/docs/schemas/roomsSchema.js b/src/routes/docs/schemas/roomsSchema.js index 39dcd8bf..c4075204 100644 --- a/src/routes/docs/schemas/roomsSchema.js +++ b/src/routes/docs/schemas/roomsSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../utils"); -const { objectId, room } = require("../../../modules/patterns"); +const { objectId, room } = require("../../../modules/patterns").default; const roomsZod = {}; roomsZod["part"] = z diff --git a/src/routes/docs/swaggerDocs.js b/src/routes/docs/swaggerDocs.js index 62639dfa..2bca48c7 100644 --- a/src/routes/docs/swaggerDocs.js +++ b/src/routes/docs/swaggerDocs.js @@ -8,7 +8,7 @@ const authReplaceDocs = require("./auth.replace"); const usersDocs = require("./users"); const roomsDocs = require("./rooms"); const chatsDocs = require("./chats"); -const { port, nodeEnv } = require("../../../loadenv"); +const { port, nodeEnv } = require("../../loadenv"); const serverList = [ { diff --git a/src/routes/docs/utils.js b/src/routes/docs/utils.js index 2f99c13c..bd006150 100644 --- a/src/routes/docs/utils.js +++ b/src/routes/docs/utils.js @@ -1,5 +1,5 @@ const { zodToJsonSchema } = require("zod-to-json-schema"); -const logger = require("../../modules/logger"); +const logger = require("../../modules/logger").default; const zodToSchemaObject = (zodObejct) => { try { diff --git a/src/routes/notifications.js b/src/routes/notifications.js index 89ec5be9..c27c22b3 100644 --- a/src/routes/notifications.js +++ b/src/routes/notifications.js @@ -3,10 +3,10 @@ const router = express.Router(); const { body } = require("express-validator"); const notificationHandlers = require("@/services/notifications"); -const validator = require("@/middlewares/validator"); +const validator = require("@/middlewares/validator").default; // 라우터 접근 시 로그인 필요 -router.use(require("@/middlewares/auth")); +router.use(require("@/middlewares/auth").default); // FCM 토큰 등록 router.post( diff --git a/src/routes/reports.js b/src/routes/reports.js index 0fc8b24a..4d76e2f2 100644 --- a/src/routes/reports.js +++ b/src/routes/reports.js @@ -1,11 +1,11 @@ const express = require("express"); -const { validateBody } = require("../middlewares/zod"); +const { validateBody } = require("@/middlewares/zod"); const { reportsZod } = require("./docs/schemas/reportsSchema"); const router = express.Router(); const reportHandlers = require("@/services/reports"); // 라우터 접근 시 로그인 필요 -router.use(require("@/middlewares/auth")); +router.use(require("@/middlewares/auth").default); router.post( "/create", diff --git a/src/routes/rooms.js b/src/routes/rooms.js index e0c8a631..60a235e2 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -3,8 +3,8 @@ const { query, body } = require("express-validator"); const router = express.Router(); const roomHandlers = require("@/services/rooms"); -const validator = require("@/middlewares/validator"); -const patterns = require("@/modules/patterns"); +const validator = require("@/middlewares/validator").default; +const patterns = require("@/modules/patterns").default; // 조건(이름, 출발지, 도착지, 날짜)에 맞는 방들을 모두 반환한다. router.get( @@ -31,7 +31,7 @@ router.get( ); // 이후 API 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth").default); // 특정 id 방 세부사항 보기 router.get( diff --git a/src/routes/users.js b/src/routes/users.js index 9bc1d4eb..aa00bcb2 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -1,7 +1,7 @@ const express = require("express"); const { body } = require("express-validator"); -const validator = require("@/middlewares/validator"); -const patterns = require("@/modules/patterns"); +const validator = require("@/middlewares/validator").default; +const patterns = require("@/modules/patterns").default; const router = express.Router(); const userHandlers = require("@/services/users"); @@ -9,7 +9,7 @@ const userHandlers = require("@/services/users"); const { replaceSpaceInNickname } = require("@/modules/modifyProfile"); // 라우터 접근 시 로그인 필요 -router.use(require("@/middlewares/auth")); +router.use(require("@/middlewares/auth").default); // 이용 약관에 동의합니다. router.post( diff --git a/src/schedules/notifyAfterArrival.js b/src/schedules/notifyAfterArrival.js index 5c1a5a6c..c2c01b25 100644 --- a/src/schedules/notifyAfterArrival.js +++ b/src/schedules/notifyAfterArrival.js @@ -1,7 +1,7 @@ const { roomModel, chatModel } = require("@/modules/stores/mongo"); // const { roomPopulateOption } = require("@/modules/populates/rooms"); const { emitChatEvent } = require("@/modules/socket"); -const logger = require("@/modules/logger"); +const logger = require("@/modules/logger").default; const MS_PER_MINUTE = 60000; diff --git a/src/schedules/notifyBeforeDepart.js b/src/schedules/notifyBeforeDepart.js index 523f6dff..b1b6e87d 100644 --- a/src/schedules/notifyBeforeDepart.js +++ b/src/schedules/notifyBeforeDepart.js @@ -1,6 +1,6 @@ const { roomModel, chatModel } = require("@/modules/stores/mongo"); const { emitChatEvent } = require("@/modules/socket"); -const logger = require("@/modules/logger"); +const logger = require("@/modules/logger").default; const MS_PER_MINUTE = 60000; diff --git a/src/services/auth.js b/src/services/auth.js index c95d263c..96de32c3 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -9,8 +9,8 @@ const { generateProfileImageUrl, getFullUsername, } = require("@/modules/modifyProfile"); -const jwt = require("@/modules/auths/jwt"); -const logger = require("@/modules/logger"); +const jwt = require("@/modules/auths/jwt").default; +const logger = require("@/modules/logger").default; // SPARCS SSO const Client = require("@/modules/auths/sparcssso"); diff --git a/src/services/auth.mobile.js b/src/services/auth.mobile.js index 7dc2798a..8e66db0e 100644 --- a/src/services/auth.mobile.js +++ b/src/services/auth.mobile.js @@ -2,8 +2,8 @@ const { userModel } = require("@/modules/stores/mongo"); const { login } = require("@/modules/auths/login"); const { registerDeviceToken, unregisterDeviceToken } = require("@/modules/fcm"); -const jwt = require("@/modules/auths/jwt"); -const logger = require("@/modules/logger"); +const jwt = require("@/modules/auths/jwt").default; +const logger = require("@/modules/logger").default; const { TOKEN_EXPIRED, TOKEN_INVALID } = require("@/loadenv").jwt; diff --git a/src/services/auth.replace.js b/src/services/auth.replace.js index 1c64e45f..65ae92c7 100644 --- a/src/services/auth.replace.js +++ b/src/services/auth.replace.js @@ -6,11 +6,11 @@ const { generateNickname, generateProfileImageUrl, } = require("@/modules/modifyProfile"); -const logger = require("@/modules/logger"); -const jwt = require("@/modules/auths/jwt"); +const logger = require("@/modules/logger").default; +const jwt = require("@/modules/auths/jwt").default; const { registerDeviceTokenHandler, tryLogin } = require("@/services/auth"); -const loginReplacePage = require("@/views/loginReplacePage"); +const loginReplacePage = require("@/views/loginReplacePage").default; const createUserData = (id) => { const info = { diff --git a/src/services/chats.js b/src/services/chats.js index c21f4b75..f8190cfd 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -1,13 +1,13 @@ const { chatModel, userModel, roomModel } = require("@/modules/stores/mongo"); const { chatPopulateOption } = require("@/modules/populates/chats"); const { roomPopulateOption } = require("@/modules/populates/rooms"); -const aws = require("@/modules/stores/aws"); +const aws = require("@/modules/stores/aws").default; const { transformChatsForRoom, emitChatEvent, emitUpdateEvent, } = require("@/modules/socket"); -const logger = require("@/modules/logger"); +const logger = require("@/modules/logger").default; const chatCount = 60; diff --git a/src/services/locations.js b/src/services/locations.js index ed81ed43..9a2c5e7d 100644 --- a/src/services/locations.js +++ b/src/services/locations.js @@ -1,5 +1,5 @@ const { locationModel } = require("@/modules/stores/mongo"); -const logger = require("@/modules/logger"); +const logger = require("@/modules/logger").default; const getAllLocationsHandler = async (_, res) => { try { diff --git a/src/services/logininfo.js b/src/services/logininfo.js index b074d847..affac40a 100644 --- a/src/services/logininfo.js +++ b/src/services/logininfo.js @@ -1,6 +1,6 @@ const { userModel } = require("@/modules/stores/mongo"); const { getLoginInfo } = require("@/modules/auths/login"); -const logger = require("@/modules/logger"); +const logger = require("@/modules/logger").default; const logininfoHandler = async (req, res) => { try { diff --git a/src/services/notifications.js b/src/services/notifications.js index 7a0b810e..fc2090cf 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -1,11 +1,11 @@ const { userModel } = require("@/modules/stores/mongo"); const { notificationOptionModel } = require("@/modules/stores/mongo"); -const logger = require("@/modules/logger"); +const logger = require("@/modules/logger").default; const { registerDeviceToken, validateDeviceToken } = require("@/modules/fcm"); // 이벤트 코드입니다. -// const { contracts } = require("../lottery"); +// const { contracts } = require("@/lottery"); const registerDeviceTokenHandler = async (req, res) => { try { diff --git a/src/services/reports.js b/src/services/reports.js index 7ca7d002..a6dfc42d 100644 --- a/src/services/reports.js +++ b/src/services/reports.js @@ -1,8 +1,8 @@ const { userModel, reportModel, roomModel } = require("@/modules/stores/mongo"); const { reportPopulateOption } = require("@/modules/populates/reports"); const { sendReportEmail } = require("@/modules/stores/aws"); -const logger = require("@/modules/logger"); -const emailPage = require("@/views/emailNoSettlementPage"); +const logger = require("@/modules/logger").default; +const emailPage = require("@/views/emailNoSettlementPage").default; const { notifyReportToReportChannel } = require("@/modules/slackNotification"); const createHandler = async (req, res) => { diff --git a/src/services/rooms.js b/src/services/rooms.js index f2dd1cbf..fe0a5cf8 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -4,7 +4,7 @@ const { userModel, } = require("@/modules/stores/mongo"); const { emitChatEvent } = require("@/modules/socket"); -const logger = require("@/modules/logger"); +const logger = require("@/modules/logger").default; const { roomPopulateOption, formatSettlement, @@ -15,12 +15,12 @@ const { } = require("@/modules/slackNotification"); // 이벤트 코드입니다. -// const { eventConfig } = require("../../loadenv"); +// const { eventConfig } = require("@/loadenv"); // const eventPeriod = eventConfig && { // startAt: new Date(eventConfig.period.startAt), // endAt: new Date(eventConfig.period.endAt), // }; -// const { contracts } = require("../lottery"); +// const { contracts } = require("@/lottery"); const createHandler = async (req, res) => { const { name, from, to, time, maxPartLength } = req.body; diff --git a/src/services/users.js b/src/services/users.js index 10008c0a..5fe9bafc 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,13 +1,13 @@ const { userModel } = require("@/modules/stores/mongo"); -const logger = require("@/modules/logger"); -const aws = require("@/modules/stores/aws"); +const logger = require("@/modules/logger").default; +const aws = require("@/modules/stores/aws").default; const { generateNickname, generateProfileImageUrl, } = require("@/modules/modifyProfile"); // 이벤트 코드입니다. -// const { contracts } = require("../lottery"); +// const { contracts } = require("@/lottery"); const agreeOnTermsOfServiceHandler = async (req, res) => { try { diff --git a/test/utils.js b/test/utils.js index e537913b..ba8d2bca 100644 --- a/test/utils.js +++ b/test/utils.js @@ -7,7 +7,7 @@ const { connectDatabase, } = require("../src/modules/stores/mongo"); const { generateProfileImageUrl } = require("../src/modules/modifyProfile"); -const { mongo: mongoUrl } = require("../loadenv"); +const { mongo: mongoUrl } = require("@/loadenv"); connectDatabase(mongoUrl); From 11c69cc650deff65b05d8ed629a774c364decb73 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Thu, 21 Mar 2024 16:40:04 +0900 Subject: [PATCH 036/151] Refactor: chat content validation --- src/modules/patterns.js | 1 + src/routes/chats.js | 7 +++---- src/routes/docs/schemas/chatsSchema.js | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/routes/docs/schemas/chatsSchema.js diff --git a/src/modules/patterns.js b/src/modules/patterns.js index 2fd1e4dc..bb24ce0a 100644 --- a/src/modules/patterns.js +++ b/src/modules/patterns.js @@ -15,5 +15,6 @@ module.exports = { chat: { chatImgType: RegExp("^(image/png|image/jpg|image/jpeg)$"), chatSendType: RegExp("^(text|account)$"), + chatContent: RegExp("\\S+"), //공백 제외 1글자 이상 }, }; diff --git a/src/routes/chats.js b/src/routes/chats.js index f689348c..f20de503 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -1,6 +1,8 @@ const express = require("express"); const { body } = require("express-validator"); const validator = require("../middlewares/validator"); +const { validateBody } = require("../middlewares/zod"); +const { chatsZod } = require("./docs/schemas/chatsSchema"); const patterns = require("../modules/patterns"); const router = express.Router(); @@ -47,10 +49,7 @@ router.post( */ router.post( "/send", - body("roomId").isMongoId(), - body("type").matches(patterns.chat.chatSendType), - body("content").isString(), - validator, + validateBody(chatsZod.sendChatHandler), chatsHandlers.sendChatHandler ); diff --git a/src/routes/docs/schemas/chatsSchema.js b/src/routes/docs/schemas/chatsSchema.js new file mode 100644 index 00000000..7e6fd978 --- /dev/null +++ b/src/routes/docs/schemas/chatsSchema.js @@ -0,0 +1,15 @@ +const { z } = require("zod"); +const { zodToSchemaObject } = require("../utils"); +const { objectId, chat } = require("../../../modules/patterns"); + +const chatsZod = { + sendChatHandler: z.object({ + roomId: z.string().regex(objectId), + type: z.string().regex(chat.chatSendType), + content: z.string().regex(chat.chatContent), + }), +}; + +const chatsSchema = zodToSchemaObject(chatsZod); + +module.exports = { chatsZod, chatsSchema }; From 1b898f3471c0c30c467e3866b24d6bfb145f3432 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Thu, 21 Mar 2024 16:40:27 +0900 Subject: [PATCH 037/151] Docs: add chatsSchema --- src/routes/docs/swaggerDocs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/docs/swaggerDocs.js b/src/routes/docs/swaggerDocs.js index 62639dfa..b0f51191 100644 --- a/src/routes/docs/swaggerDocs.js +++ b/src/routes/docs/swaggerDocs.js @@ -1,5 +1,6 @@ const { reportsSchema } = require("./schemas/reportsSchema"); const { roomsSchema } = require("./schemas/roomsSchema"); +const { chatsSchema } = require("./schemas/chatsSchema"); const reportsDocs = require("./reports"); const logininfoDocs = require("./logininfo"); const locationsDocs = require("./locations"); @@ -85,6 +86,7 @@ const swaggerDocs = { schemas: { ...reportsSchema, ...roomsSchema, + ...chatsSchema, }, }, }; From a1bab8dab0b5cb25d7fc1d5324c814d9a51d2daf Mon Sep 17 00:00:00 2001 From: ybmin Date: Thu, 21 Mar 2024 17:08:10 +0900 Subject: [PATCH 038/151] Refactor: zod migration --- src/routes/docs/schemas/fareSchema.js | 34 ++--- src/routes/fare.js | 9 +- src/services/fare.js | 197 +++++++++++++++++--------- 3 files changed, 146 insertions(+), 94 deletions(-) diff --git a/src/routes/docs/schemas/fareSchema.js b/src/routes/docs/schemas/fareSchema.js index b725c42a..27f63450 100644 --- a/src/routes/docs/schemas/fareSchema.js +++ b/src/routes/docs/schemas/fareSchema.js @@ -1,24 +1,14 @@ -const { objectIdPattern } = require("../utils"); +const { z } = require("zod"); +const { zodToSchemaObject } = require("../utils"); +const { objectId } = require("../../../modules/patterns"); -const fareSchema = { - getTaxiFare: { - type: "object", - required: ["from", "to", "time"], - properties: { - from: { - type: "string", - format: objectIdPattern, - }, - to: { - type: "string", - format: objectIdPattern, - }, - time: { - type: "string", - format: "date-time", - }, - }, - errorMessage: "validation: bad request", - }, +const fareZod = { + getTaxiFare: z.object({ + from: z.string().regex(objectId), + to: z.string().regex(objectId), + time: z.string().datetime(), + }), }; -module.exports = fareSchema; +const fareSchema = zodToSchemaObject(fareZod); + +module.exports = { fareSchema, fareZod }; diff --git a/src/routes/fare.js b/src/routes/fare.js index 7441d0ee..42a0c75c 100644 --- a/src/routes/fare.js +++ b/src/routes/fare.js @@ -1,12 +1,11 @@ const express = require("express"); -const fareSchema = require("./docs/schemas/fareSchema"); -const { validateQuery } = require("../middlewares/ajv"); -const router = express.Router(); - +const { validateQuery } = require("../middlewares/zod"); +const { fareZod } = require("./docs/schemas/fareSchema"); const { getTaxiFare, initDatabase } = require("../services/fare"); +const router = express.Router(); router.post("/init", initDatabase); -router.get("/getTaxiFare", validateQuery(fareSchema.getTaxiFare), getTaxiFare); +router.get("/getTaxiFare", validateQuery(fareZod.getTaxiFare), getTaxiFare); module.exports = router; diff --git a/src/services/fare.js b/src/services/fare.js index f7ed765f..15c949fb 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -50,44 +50,77 @@ const initDatabase = async (req, res) => { }); }); } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { - const fare = ( - await axios.get( + await axios + .get( `${ naverCloudApiCall + from.longitude + "," + from.latitude }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, { headers: naverCloudApi } ) - ).data.route.traoptimal[0].summary.taxiFare; - [...Array(48 * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, + .then((res) => { + [...Array(48 * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: res.data.route.traoptimal[0].summary.taxiFare, + isMajor: true, + }); + }); + }) + .catch((err) => { + logger.error(err.message); + [...Array(48 * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: 0, + isMajor: true, + }); + }); }); - }); } // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 else { - const fare = ( - await axios.get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverCloudApi } + await new Promise(() => + setTimeout( + async () => + await axios + .get( + `${ + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverCloudApi } + ) + .then((res) => { + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * 48, + fare: res.data.route.traoptimal[0].summary.taxiFare, + isMajor: false, + }); + }); + }) + .catch((err) => { + logger.error(err.message); + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * 48, + fare: 0, + isMajor: false, + }); + }); + }), + 100 ) - ).data.route.traoptimal[0].summary.taxiFare; - setTimeout(() => {}, 100); - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * 48, - fare: fare, - isMajor: false, - }); - }); + ); } await taxiFareModel.insertMany(tableFare); }); @@ -109,16 +142,17 @@ const initDatabase = async (req, res) => { */ const getTaxiFare = async (req, res) => { try { - const from = await locationModel.findOne({ - _id: { $eq: req.query.from }, - }); + const from = await locationModel + .findOne({ + _id: { $eq: req.query.from }, + }) + .clone(); const to = await locationModel .findOne({ _id: { $eq: req.query.to } }) .clone(); const sTime = scaledTime(new Date(req.query.time)); if (!from || !to) { - console.log("asds"); res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); return; } @@ -128,7 +162,6 @@ const getTaxiFare = async (req, res) => { (from.koName === "카이스트 본원" && to.koName === "대전역") || (from.koName === "대전역" && to.koName === "카이스트 본원") ) { - console.log("asds"); const taxiFare = await taxiFareModel .findOne( { @@ -146,17 +179,23 @@ const getTaxiFare = async (req, res) => { .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare === 0) { - const fare = ( - await axios.get( + await axios + .get( `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ to.longitude + "," + to.latitude }&options=traoptimal`, { headers: naverCloudApi } ) - ).data.route.traoptimal[0].summary.taxiFare; - res.state(200).send(fare); + .then((text) => { + res + .status(200) + .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + }) + .catch((err) => { + logger.error(err.message); + }); } else { - res.state(200).send(taxiFare.fare); + res.status(200).json({ fare: taxiFare.fare }); } } // 카이스트 본원 <-> 대전역이 아닌 경우 @@ -176,20 +215,25 @@ const getTaxiFare = async (req, res) => { } ) .clone(); - console.log(taxiFare.fare); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare === 0) { - const fare = ( - await axios.get( + if (taxiFare === undefined) { + await axios + .get( `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ to.longitude + "," + to.latitude }&options=traoptimal`, { headers: naverCloudApi } ) - ).data.route.traoptimal[0].summary.taxiFare; - res.send(fare); + .then((text) => { + res + .status(200) + .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + }) + .catch((err) => { + logger.error(err.message); + }); } else { - res.send(taxiFare.fare); + res.status(200).json({ fare: taxiFare.fare }); } } } catch (err) { @@ -208,30 +252,49 @@ const getTaxiFare = async (req, res) => { * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 여부 */ const updateTaxiFare = async (sTime, isMajor) => { - const prevFares = await taxiFareModel.find({ - time: sTime, - isMajor: isMajor, - }); + const prevFares = await taxiFareModel + .find({ + time: sTime, + isMajor: isMajor, + }) + .clone(); prevFares.map(async (item) => { - const from = await locationModel.findOne({ _id: item.from }); - const to = await locationModel.findOne({ _id: item.to }); - const fare = ( - await axios.get( - `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverCloudApi } + const from = await locationModel.findOne({ _id: item.from }).clone(); + const to = await locationModel.findOne({ _id: item.to }).clone(); + + await new Promise(() => + setTimeout( + async () => + await axios + .get( + `${ + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverCloudApi } + ) + .catch((err) => { + logger.error(err.message); + }) + .then(async (res) => { + await taxiFareModel + .updateOne( + { from: item.from, to: item.to, time: sTime }, + { fare: res.data.route.traoptimal[0].summary.taxiFare }, + function (err, docs) { + if (err) + logger.error( + "Error occured while updating TaxiFare document: " + + err.message + ); + } + ) + .clone(); + }) + .catch((err) => { + logger.error(err.message); + }), + 100 ) - ).data.route.traoptimal[0].summary.taxiFare; - await taxiFareModel.updateOne( - { from: item.from, to: item.to, time: sTime }, - { fare: fare }, - function (err, docs) { - if (err) - logger.error( - "Error occured while updating TaxiFare document: " + err.message - ); - } ); }); }; From 4c884423ec4a2a3a95da41140fd833ae91a1b22a Mon Sep 17 00:00:00 2001 From: ybmin Date: Thu, 21 Mar 2024 17:18:16 +0900 Subject: [PATCH 039/151] Docs: change to naver api optional --- loadenv.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/loadenv.js b/loadenv.js index 9269f3e1..4d6e5a7c 100644 --- a/loadenv.js +++ b/loadenv.js @@ -55,7 +55,6 @@ module.exports = { endAt: "2024-03-19T00:00:00+09:00", }, }, // optional - // Naver Cloud Platform Maps Directions 5 API Keys - naverCloudApiId: process.env.NAVER_MAP_API_ID, //required - naverCloudApiKey: process.env.NAVER_MAP_API_KEY, //required + naverCloudApiId: process.env.NAVER_MAP_API_ID, // optional + naverCloudApiKey: process.env.NAVER_MAP_API_KEY, //optional }; From f11d01c428a8da91070cf8f71afb57b19bb49195 Mon Sep 17 00:00:00 2001 From: ybmin Date: Thu, 21 Mar 2024 18:41:41 +0900 Subject: [PATCH 040/151] Add: module init code & exception case --- app.js | 3 + loadenv.js | 4 +- src/modules/fare.js | 142 +++++++++++++++++++++++++++++++++++++++- src/routes/docs/fare.js | 27 -------- src/routes/fare.js | 4 +- src/services/fare.js | 131 ++++-------------------------------- 6 files changed, 159 insertions(+), 152 deletions(-) diff --git a/app.js b/app.js index e66ddcf3..1ac7310e 100644 --- a/app.js +++ b/app.js @@ -86,3 +86,6 @@ app.set("io", startSocketServer(serverHttp)); // [Schedule] 스케줄러 시작 require("./src/schedules")(app); + +// [Module] 택시 예상 비용 db 초기화 +require("./src/modules/fare").initDatabase(); diff --git a/loadenv.js b/loadenv.js index 4d6e5a7c..4e43740a 100644 --- a/loadenv.js +++ b/loadenv.js @@ -55,6 +55,6 @@ module.exports = { endAt: "2024-03-19T00:00:00+09:00", }, }, // optional - naverCloudApiId: process.env.NAVER_MAP_API_ID, // optional - naverCloudApiKey: process.env.NAVER_MAP_API_KEY, //optional + naverCloudApiId: process.env.NAVER_MAP_API_ID || "none", // optional + naverCloudApiKey: process.env.NAVER_MAP_API_KEY || "none", //optional }; diff --git a/src/modules/fare.js b/src/modules/fare.js index 14b9d3f9..9f8ea96a 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -1,3 +1,9 @@ +const logger = require("./logger"); +const axios = require("axios"); + +const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); +const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); +const { scaledTime } = require("../modules/fare"); /** * 시간을 받아서 30분 단위로 변환해서 반환합니다. * 요일 정보도 하나로 관리 @@ -10,4 +16,138 @@ const scaledTime = (time) => { ); }; -module.exports = { scaledTime }; +// Naver Cloud Platform Maps Directions 5 API Keys +const naverCloudApi = { + "X-NCP-APIGW-API-KEY-ID": naverCloudApiId, + "X-NCP-APIGW-API-KEY": naverCloudApiKey, +}; +const naverCloudApiCall = + "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; + +/** Initialize database + * 1. Erase all previous data + * 2. Sets all taxi fare to 0 + * @summary 카이스트 본원 <-> 대전역의 경우 48 * 7개의 시간대에 대한 택시 요금을 init 시점의 비용으로 설정합니다. + * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 fare를 init 시점의 비용으로 설정합니다. time은 0으로 설정합니다. + */ +const initDatabase = async () => { + try { + if ( + naverCloudApi["X-NCP-APIGW-API-KEY"] == "none" || + naverCloudApi["X-NCP-APIGW-API-KEY-ID"] == "none" + ) { + logger.log( + "Naver Cloud API가 존재하지 않습니다.택시 비용 관련 기능을 사용할 수 없습니다." + ); + return; + } + // Remove all previous data + await taxiFareModel.deleteMany({}); + + const location = await locationModel.find({ isValid: { $eq: true } }); + + location.map((from) => { + location.map(async (to) => { + if (from._id === to._id) return; + let tableFare = []; + // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 일괄적으로 설정 + if (from.koName === "카이스트 본원" && to.koName === "대전역") { + const fare = ( + await axios.get( + `${ + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverCloudApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(48 * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: fare, + isMajor: true, + }); + }); + } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { + await axios + .get( + `${ + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverCloudApi } + ) + .then((res) => { + [...Array(48 * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: res.data.route.traoptimal[0].summary.taxiFare, + isMajor: true, + }); + }); + }) + .catch((err) => { + logger.error(err.message); + [...Array(48 * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: 0, + isMajor: true, + }); + }); + }); + } + // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 + else { + await new Promise(() => + setTimeout( + async () => + await axios + .get( + `${ + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverCloudApi } + ) + .then((res) => { + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * 48, + fare: res.data.route.traoptimal[0].summary.taxiFare, + isMajor: false, + }); + }); + }) + .catch((err) => { + logger.error(err.message); + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * 48, + fare: 0, + isMajor: false, + }); + }); + }), + 100 + ) + ); + } + await taxiFareModel.insertMany(tableFare); + }); + }); + } catch (err) { + logger.error("Error occured while initializing database: " + err.message); + } +}; + +module.exports = { scaledTime, initDatabase }; diff --git a/src/routes/docs/fare.js b/src/routes/docs/fare.js index bda96260..4f349bd4 100644 --- a/src/routes/docs/fare.js +++ b/src/routes/docs/fare.js @@ -5,33 +5,6 @@ const tag = "fare"; const apiPrefix = "/fare"; const fareDocs = {}; -fareDocs[`${apiPrefix}/init`] = { - post: { - tags: [tag], - summary: "택시 요금 db 초기화", - }, - response: { - 200: { - description: "TaxiFare Database initialized", - content: { - "text/plain": { - schema: { - type: "string", - example: "TaxiFare Database initialized", - }, - }, - }, - }, - 500: { - description: "TaxiFare Database failed", - content: { - "text/html": { - example: "fare/init : TaxiFare Database failed", - }, - }, - }, - }, -}; fareDocs[`${apiPrefix}/getTaxiFare`] = { get: { diff --git a/src/routes/fare.js b/src/routes/fare.js index 42a0c75c..2d66e34a 100644 --- a/src/routes/fare.js +++ b/src/routes/fare.js @@ -1,11 +1,9 @@ const express = require("express"); const { validateQuery } = require("../middlewares/zod"); const { fareZod } = require("./docs/schemas/fareSchema"); -const { getTaxiFare, initDatabase } = require("../services/fare"); +const { getTaxiFare } = require("../services/fare"); const router = express.Router(); -router.post("/init", initDatabase); - router.get("/getTaxiFare", validateQuery(fareZod.getTaxiFare), getTaxiFare); module.exports = router; diff --git a/src/services/fare.js b/src/services/fare.js index 15c949fb..7712e5dc 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -13,124 +13,6 @@ const naverCloudApi = { const naverCloudApiCall = "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; -/** Initialize database - * 1. Erase all previous data - * 2. Sets all taxi fare to 0 - * @summary 카이스트 본원 <-> 대전역의 경우 48 * 7개의 시간대에 대한 택시 요금을 init 시점의 비용으로 설정합니다. - * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 fare를 init 시점의 비용으로 설정합니다. time은 0으로 설정합니다. - */ -const initDatabase = async (req, res) => { - try { - // Remove all previous data - await taxiFareModel.deleteMany({}); - - const location = await locationModel.find({ isValid: { $eq: true } }); - - location.map((from) => { - location.map(async (to) => { - if (from._id === to._id) return; - let tableFare = []; - // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 일괄적으로 설정 - if (from.koName === "카이스트 본원" && to.koName === "대전역") { - const fare = ( - await axios.get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverCloudApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; - [...Array(48 * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, - }); - }); - } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { - await axios - .get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverCloudApi } - ) - .then((res) => { - [...Array(48 * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: res.data.route.traoptimal[0].summary.taxiFare, - isMajor: true, - }); - }); - }) - .catch((err) => { - logger.error(err.message); - [...Array(48 * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: 0, - isMajor: true, - }); - }); - }); - } - // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 - else { - await new Promise(() => - setTimeout( - async () => - await axios - .get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverCloudApi } - ) - .then((res) => { - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * 48, - fare: res.data.route.traoptimal[0].summary.taxiFare, - isMajor: false, - }); - }); - }) - .catch((err) => { - logger.error(err.message); - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * 48, - fare: 0, - isMajor: false, - }); - }); - }), - 100 - ) - ); - } - await taxiFareModel.insertMany(tableFare); - }); - }); - res.status(200).send("TaxiFare Database initialized"); - } catch (err) { - res.status(500).json({ error: "fare/init: TaxiFare Database failed" }); - } -}; - /** * 주어진 from, to, time에 대한 택시 요금을 반환합니다. * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다. @@ -142,6 +24,18 @@ const initDatabase = async (req, res) => { */ const getTaxiFare = async (req, res) => { try { + if ( + naverCloudApi["X-NCP-APIGW-API-KEY"] == "none" || + naverCloudApi["X-NCP-APIGW-API-KEY-ID"] == "none" + ) { + logger.log( + "Naver Cloud API가 존재하지 않습니다.택시 비용 관련 기능을 사용할 수 없습니다." + ); + res + .status(503) + .json({ error: "fare/getTaxiFare: Naver Cloud API not found" }); + return; + } const from = await locationModel .findOne({ _id: { $eq: req.query.from }, @@ -300,7 +194,6 @@ const updateTaxiFare = async (sTime, isMajor) => { }; module.exports = { - initDatabase, getTaxiFare, updateTaxiFare, }; From c7603babdf741b8efd863f79ae8fa234761d6751 Mon Sep 17 00:00:00 2001 From: ybmin Date: Mon, 25 Mar 2024 20:42:37 +0900 Subject: [PATCH 041/151] Fix: remove import --- src/modules/fare.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 9f8ea96a..3e370cc7 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -3,7 +3,6 @@ const axios = require("axios"); const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); -const { scaledTime } = require("../modules/fare"); /** * 시간을 받아서 30분 단위로 변환해서 반환합니다. * 요일 정보도 하나로 관리 From 671a07ececdb99d506f499cf5073fa98980709b6 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 26 Mar 2024 20:22:13 +0900 Subject: [PATCH 042/151] Fix: naver api axios 429 error --- src/modules/fare.js | 92 +++++++++++++++++++++----------------------- src/services/fare.js | 69 ++++++++++++++++----------------- 2 files changed, 77 insertions(+), 84 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 3e370cc7..d8df4091 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -3,6 +3,15 @@ const axios = require("axios"); const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); + +// Naver Cloud Platform Maps Directions 5 API Keys +const naverCloudApi = { + "X-NCP-APIGW-API-KEY-ID": naverCloudApiId, + "X-NCP-APIGW-API-KEY": naverCloudApiKey, +}; +const naverCloudApiCall = + "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; + /** * 시간을 받아서 30분 단위로 변환해서 반환합니다. * 요일 정보도 하나로 관리 @@ -15,14 +24,6 @@ const scaledTime = (time) => { ); }; -// Naver Cloud Platform Maps Directions 5 API Keys -const naverCloudApi = { - "X-NCP-APIGW-API-KEY-ID": naverCloudApiId, - "X-NCP-APIGW-API-KEY": naverCloudApiKey, -}; -const naverCloudApiCall = - "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; - /** Initialize database * 1. Erase all previous data * 2. Sets all taxi fare to 0 @@ -45,8 +46,9 @@ const initDatabase = async () => { const location = await locationModel.find({ isValid: { $eq: true } }); - location.map((from) => { - location.map(async (to) => { + location.map(async (from) => { + await location.reduce(async (acc, to) => { + await acc.then(); if (from._id === to._id) return; let tableFare = []; // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 일괄적으로 설정 @@ -102,47 +104,41 @@ const initDatabase = async () => { } // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 else { - await new Promise(() => - setTimeout( - async () => - await axios - .get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverCloudApi } - ) - .then((res) => { - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * 48, - fare: res.data.route.traoptimal[0].summary.taxiFare, - isMajor: false, - }); - }); - }) - .catch((err) => { - logger.error(err.message); - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * 48, - fare: 0, - isMajor: false, - }); - }); - }), - 100 + await axios + .get( + `${ + naverCloudApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverCloudApi } ) - ); + .then((res) => { + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * 48, + fare: res.data.route.traoptimal[0].summary.taxiFare, + isMajor: false, + }); + }); + }) + .catch((err) => { + logger.error(err.message); + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * 48, + fare: 0, + isMajor: false, + }); + }); + }); } await taxiFareModel.insertMany(tableFare); - }); + await new Promise((resolve) => setTimeout(resolve, 200)); + return acc; + }, Promise.resolve()); }); } catch (err) { logger.error("Error occured while initializing database: " + err.message); diff --git a/src/services/fare.js b/src/services/fare.js index 7712e5dc..dcaaf17b 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -110,7 +110,7 @@ const getTaxiFare = async (req, res) => { ) .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (taxiFare === undefined) { + if (!taxiFare || taxiFare.fare === 0) { await axios .get( `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ @@ -152,45 +152,42 @@ const updateTaxiFare = async (sTime, isMajor) => { isMajor: isMajor, }) .clone(); - prevFares.map(async (item) => { + await prevFares.reduce(async (acc, item) => { const from = await locationModel.findOne({ _id: item.from }).clone(); const to = await locationModel.findOne({ _id: item.to }).clone(); - await new Promise(() => - setTimeout( - async () => - await axios - .get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverCloudApi } - ) - .catch((err) => { - logger.error(err.message); - }) - .then(async (res) => { - await taxiFareModel - .updateOne( - { from: item.from, to: item.to, time: sTime }, - { fare: res.data.route.traoptimal[0].summary.taxiFare }, - function (err, docs) { - if (err) - logger.error( - "Error occured while updating TaxiFare document: " + - err.message - ); - } - ) - .clone(); - }) - .catch((err) => { - logger.error(err.message); - }), - 100 + await acc.then(); + await axios + .get( + `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverCloudApi } ) - ); - }); + .catch((err) => { + logger.error(err.message); + }) + .then(async (res) => { + await taxiFareModel + .updateOne( + { from: item.from, to: item.to, time: sTime }, + { fare: res.data.route.traoptimal[0].summary.taxiFare }, + function (err, docs) { + if (err) + logger.error( + "Error occured while updating TaxiFare document: " + + err.message + ); + } + ) + .clone(); + }) + .catch((err) => { + logger.error(err.message); + }); + await new Promise((resolve) => setTimeout(() => resolve, 200)); + return acc; + }, Promise.resolve()); }; module.exports = { From 5f58f324be648f9a64626716f25a771a58d0ca78 Mon Sep 17 00:00:00 2001 From: hwmin414 Date: Tue, 26 Mar 2024 23:50:02 +0900 Subject: [PATCH 043/151] feat: chat content regex sync with client --- src/modules/patterns.js | 3 ++- src/routes/docs/schemas/chatsSchema.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/patterns.js b/src/modules/patterns.js index bb24ce0a..0d526dd8 100644 --- a/src/modules/patterns.js +++ b/src/modules/patterns.js @@ -15,6 +15,7 @@ module.exports = { chat: { chatImgType: RegExp("^(image/png|image/jpg|image/jpeg)$"), chatSendType: RegExp("^(text|account)$"), - chatContent: RegExp("\\S+"), //공백 제외 1글자 이상 + chatContent: RegExp("^\\s{0,}\\S{1}[\\s\\S]{0,}$"), // 왼쪽 공백 제외 최소 1개 문자 + chatContentLength: RegExp("^[\\s\\S]{1,140}$"), // 공백 포함 최대 140문자 }, }; diff --git a/src/routes/docs/schemas/chatsSchema.js b/src/routes/docs/schemas/chatsSchema.js index 7e6fd978..fa0ae257 100644 --- a/src/routes/docs/schemas/chatsSchema.js +++ b/src/routes/docs/schemas/chatsSchema.js @@ -6,7 +6,7 @@ const chatsZod = { sendChatHandler: z.object({ roomId: z.string().regex(objectId), type: z.string().regex(chat.chatSendType), - content: z.string().regex(chat.chatContent), + content: z.string().regex(chat.chatContent).regex(chat.chatContentLength), }), }; From 17ac485bef03e074f784f4c8d8cef68d182b7755 Mon Sep 17 00:00:00 2001 From: ybmin Date: Wed, 27 Mar 2024 00:23:42 +0900 Subject: [PATCH 044/151] Fix: env file --- loadenv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loadenv.js b/loadenv.js index b4762a2f..7fc2508d 100644 --- a/loadenv.js +++ b/loadenv.js @@ -1,5 +1,5 @@ // 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옴 -require("dotenv").config({ path: `./.env.development` }); +require("dotenv").config({ path: `./.env.${process.env.NODE_ENV}` }); module.exports = { nodeEnv: process.env.NODE_ENV, // required ("production" or "development" or "test") From 836e49eb99edaafe2cd2cc65b0bbdd8eb8d8ff8e Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 23 Apr 2024 22:11:41 +0900 Subject: [PATCH 045/151] Fix: naver api key none to null --- loadenv.js | 4 ++-- src/modules/fare.js | 4 ++-- src/services/fare.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/loadenv.js b/loadenv.js index 7fc2508d..0419e457 100644 --- a/loadenv.js +++ b/loadenv.js @@ -44,6 +44,6 @@ module.exports = { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional - naverCloudApiId: process.env.NAVER_MAP_API_ID || "none", // optional - naverCloudApiKey: process.env.NAVER_MAP_API_KEY || "none", //optional + naverCloudApiId: process.env.NAVER_MAP_API_ID, // optional + naverCloudApiKey: process.env.NAVER_MAP_API_KEY, //optional }; diff --git a/src/modules/fare.js b/src/modules/fare.js index d8df4091..72587962 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -33,8 +33,8 @@ const scaledTime = (time) => { const initDatabase = async () => { try { if ( - naverCloudApi["X-NCP-APIGW-API-KEY"] == "none" || - naverCloudApi["X-NCP-APIGW-API-KEY-ID"] == "none" + naverCloudApi["X-NCP-APIGW-API-KEY"] === null || + naverCloudApi["X-NCP-APIGW-API-KEY-ID"] === null ) { logger.log( "Naver Cloud API가 존재하지 않습니다.택시 비용 관련 기능을 사용할 수 없습니다." diff --git a/src/services/fare.js b/src/services/fare.js index dcaaf17b..99463386 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -25,8 +25,8 @@ const naverCloudApiCall = const getTaxiFare = async (req, res) => { try { if ( - naverCloudApi["X-NCP-APIGW-API-KEY"] == "none" || - naverCloudApi["X-NCP-APIGW-API-KEY-ID"] == "none" + naverCloudApi["X-NCP-APIGW-API-KEY"] === null || + naverCloudApi["X-NCP-APIGW-API-KEY-ID"] === null ) { logger.log( "Naver Cloud API가 존재하지 않습니다.택시 비용 관련 기능을 사용할 수 없습니다." From da9de887fb351ab53c81a1fa11b853da6092b373 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 30 Apr 2024 20:57:20 +0900 Subject: [PATCH 046/151] Refactor: merge two package.json files into one file --- package.json | 4 +- src/sampleGenerator/.gitignore | 107 --------------------------------- src/sampleGenerator/index.js | 2 +- tsconfig.json | 1 + 4 files changed, 5 insertions(+), 109 deletions(-) delete mode 100644 src/sampleGenerator/.gitignore diff --git a/package.json b/package.json index 19f05815..e28269bb 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node dist/index.js", "lint": "pnpm eslint .", "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production node", - "sample": "cd src/sampleGenerator && npm start && cd .." + "sample": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/index.js", + "dumpDB": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/tools/dump.js", + "restoreDB": "node src/sampleGenerator/tools/restore.js" }, "engines": { "node": ">=18.0.0", diff --git a/src/sampleGenerator/.gitignore b/src/sampleGenerator/.gitignore deleted file mode 100644 index 2909449b..00000000 --- a/src/sampleGenerator/.gitignore +++ /dev/null @@ -1,107 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# MongoDB Dump -dump/ diff --git a/src/sampleGenerator/index.js b/src/sampleGenerator/index.js index e83a9335..c1168dab 100644 --- a/src/sampleGenerator/index.js +++ b/src/sampleGenerator/index.js @@ -10,7 +10,7 @@ const { mongo: mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); const database = connectDatabase(mongoUrl); const fs = require("fs"); -const sampleData = JSON.parse(fs.readFileSync("./sampleData.json")); +const sampleData = require("./sampleData.json"); const main = async () => { await database.db.dropDatabase(); diff --git a/tsconfig.json b/tsconfig.json index 0f059817..0c842280 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "moduleResolution": "node16", "allowJs": true, "outDir": "./dist", + "resolveJsonModule": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, From b63c72091dfd5923e7912dc156e26c6885f34a13 Mon Sep 17 00:00:00 2001 From: ybmin Date: Wed, 1 May 2024 22:39:45 +0900 Subject: [PATCH 047/151] Refactor: review contents --- app.js | 2 +- loadenv.js | 4 +-- src/modules/fare.js | 57 +++++++++++++++++++++------------------- src/services/fare.js | 62 +++++++++++++++++++++++++------------------- 4 files changed, 69 insertions(+), 56 deletions(-) diff --git a/app.js b/app.js index 37ae32c1..ef4fc765 100644 --- a/app.js +++ b/app.js @@ -88,4 +88,4 @@ app.set("io", startSocketServer(serverHttp)); require("./src/schedules")(app); // [Module] 택시 예상 비용 db 초기화 -require("./src/modules/fare").initDatabase(); +// require("./src/modules/fare").initDatabase(); diff --git a/loadenv.js b/loadenv.js index 0419e457..930c2982 100644 --- a/loadenv.js +++ b/loadenv.js @@ -44,6 +44,6 @@ module.exports = { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional - naverCloudApiId: process.env.NAVER_MAP_API_ID, // optional - naverCloudApiKey: process.env.NAVER_MAP_API_KEY, //optional + naverMapApiId: process.env.NAVER_MAP_API_ID, // optional + naverMapApiKey: process.env.NAVER_MAP_API_KEY, //optional }; diff --git a/src/modules/fare.js b/src/modules/fare.js index 72587962..d5719c5a 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -1,17 +1,20 @@ const logger = require("./logger"); const axios = require("axios"); -const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); +const { naverMapApiId, naverMapApiKey } = require("../../loadenv"); const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); // Naver Cloud Platform Maps Directions 5 API Keys -const naverCloudApi = { - "X-NCP-APIGW-API-KEY-ID": naverCloudApiId, - "X-NCP-APIGW-API-KEY": naverCloudApiKey, +const naverMapApi = { + "X-NCP-APIGW-API-KEY-ID": naverMapApiId, + "X-NCP-APIGW-API-KEY": naverMapApiKey, }; -const naverCloudApiCall = +const naverMapApiCall = "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; +// scaledTime에 사용하는 상수입니다. 0 ~ 47 (0:00 ~ 23:30) +const timeConstants = 48; + /** * 시간을 받아서 30분 단위로 변환해서 반환합니다. * 요일 정보도 하나로 관리 @@ -20,7 +23,9 @@ const naverCloudApiCall = */ const scaledTime = (time) => { return ( - 48 * time.getDay() + time.getHours() * 2 + (time.getMinutes() >= 30 ? 1 : 0) + timeConstants * time.getDay() + + time.getHours() * 2 + + (time.getMinutes() >= 30 ? 1 : 0) ); }; @@ -33,8 +38,8 @@ const scaledTime = (time) => { const initDatabase = async () => { try { if ( - naverCloudApi["X-NCP-APIGW-API-KEY"] === null || - naverCloudApi["X-NCP-APIGW-API-KEY-ID"] === null + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.log( "Naver Cloud API가 존재하지 않습니다.택시 비용 관련 기능을 사용할 수 없습니다." @@ -55,13 +60,13 @@ const initDatabase = async () => { if (from.koName === "카이스트 본원" && to.koName === "대전역") { const fare = ( await axios.get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverCloudApi } + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } ) ).data.route.traoptimal[0].summary.taxiFare; - [...Array(48 * 7)].map((_, i) => { + [...Array(timeConstants * 7)].map((_, i) => { tableFare.push({ from: from._id, to: to._id, @@ -73,13 +78,13 @@ const initDatabase = async () => { } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { await axios .get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverCloudApi } + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } ) .then((res) => { - [...Array(48 * 7)].map((_, i) => { + [...Array(timeConstants * 7)].map((_, i) => { tableFare.push({ from: from._id, to: to._id, @@ -91,7 +96,7 @@ const initDatabase = async () => { }) .catch((err) => { logger.error(err.message); - [...Array(48 * 7)].map((_, i) => { + [...Array(timeConstants * 7)].map((_, i) => { tableFare.push({ from: from._id, to: to._id, @@ -106,17 +111,17 @@ const initDatabase = async () => { else { await axios .get( - `${ - naverCloudApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverCloudApi } + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } ) .then((res) => { [...Array(7)].map((_, i) => { tableFare.push({ from: from, to: to, - time: i * 48, + time: i * timeConstants, fare: res.data.route.traoptimal[0].summary.taxiFare, isMajor: false, }); @@ -128,7 +133,7 @@ const initDatabase = async () => { tableFare.push({ from: from, to: to, - time: i * 48, + time: i * timeConstants, fare: 0, isMajor: false, }); @@ -138,7 +143,7 @@ const initDatabase = async () => { await taxiFareModel.insertMany(tableFare); await new Promise((resolve) => setTimeout(resolve, 200)); return acc; - }, Promise.resolve()); + }); }); } catch (err) { logger.error("Error occured while initializing database: " + err.message); diff --git a/src/services/fare.js b/src/services/fare.js index 99463386..1e0073f3 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -1,16 +1,16 @@ const axios = require("axios"); -const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv"); +const { naverMapApiId, naverMapApiKey } = require("../../loadenv"); const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); const { scaledTime } = require("../modules/fare"); const logger = require("../modules/logger"); // Naver Cloud Platform Maps Directions 5 API Keys -const naverCloudApi = { - "X-NCP-APIGW-API-KEY-ID": naverCloudApiId, - "X-NCP-APIGW-API-KEY": naverCloudApiKey, +const naverMapApi = { + "X-NCP-APIGW-API-KEY-ID": naverMapApiId, + "X-NCP-APIGW-API-KEY": naverMapApiKey, }; -const naverCloudApiCall = +const naverMapApiCall = "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; /** @@ -25,11 +25,11 @@ const naverCloudApiCall = const getTaxiFare = async (req, res) => { try { if ( - naverCloudApi["X-NCP-APIGW-API-KEY"] === null || - naverCloudApi["X-NCP-APIGW-API-KEY-ID"] === null + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.log( - "Naver Cloud API가 존재하지 않습니다.택시 비용 관련 기능을 사용할 수 없습니다." + "There is no credential for Naver Map. Taxi Fare functions are disabled." ); res .status(503) @@ -50,12 +50,22 @@ const getTaxiFare = async (req, res) => { res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); return; } - - // 카이스트 본원 <-> 대전역 - if ( - (from.koName === "카이스트 본원" && to.koName === "대전역") || - (from.koName === "대전역" && to.koName === "카이스트 본원") - ) { + const isMajor = ( + await taxiFareModel + .findOne( + { from: from._id, to: to._id, time: 0 }, + { isMajor: true }, + (err, docs) => { + if (err) + logger.error( + "Error occured while finding TaxiFare documents: " + err.message + ); + } + ) + .clone() + ).isMajor; + // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) + if (isMajor) { const taxiFare = await taxiFareModel .findOne( { @@ -63,7 +73,7 @@ const getTaxiFare = async (req, res) => { to: to._id, time: sTime, }, - function (err, docs) { + (err, docs) => { if (err) logger.error( "Error occured while finding TaxiFare documents: " + err.message @@ -75,10 +85,10 @@ const getTaxiFare = async (req, res) => { if (!taxiFare || taxiFare.fare === 0) { await axios .get( - `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ to.longitude + "," + to.latitude }&options=traoptimal`, - { headers: naverCloudApi } + { headers: naverMapApi } ) .then((text) => { res @@ -91,9 +101,7 @@ const getTaxiFare = async (req, res) => { } else { res.status(200).json({ fare: taxiFare.fare }); } - } - // 카이스트 본원 <-> 대전역이 아닌 경우 - else { + } else { const taxiFare = await taxiFareModel .findOne( { @@ -101,7 +109,7 @@ const getTaxiFare = async (req, res) => { to: to._id, time: 0, }, - function (err, docs) { + (err, docs) => { if (err) logger.error( "Error occured while finding TaxiFare documents: " + err.message @@ -113,10 +121,10 @@ const getTaxiFare = async (req, res) => { if (!taxiFare || taxiFare.fare === 0) { await axios .get( - `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ to.longitude + "," + to.latitude }&options=traoptimal`, - { headers: naverCloudApi } + { headers: naverMapApi } ) .then((text) => { res @@ -159,10 +167,10 @@ const updateTaxiFare = async (sTime, isMajor) => { await acc.then(); await axios .get( - `${naverCloudApiCall + from.longitude + "," + from.latitude}&goal=${ + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ to.longitude + "," + to.latitude }&options=traoptimal`, - { headers: naverCloudApi } + { headers: naverMapApi } ) .catch((err) => { logger.error(err.message); @@ -172,7 +180,7 @@ const updateTaxiFare = async (sTime, isMajor) => { .updateOne( { from: item.from, to: item.to, time: sTime }, { fare: res.data.route.traoptimal[0].summary.taxiFare }, - function (err, docs) { + (err, docs) => { if (err) logger.error( "Error occured while updating TaxiFare document: " + @@ -187,7 +195,7 @@ const updateTaxiFare = async (sTime, isMajor) => { }); await new Promise((resolve) => setTimeout(() => resolve, 200)); return acc; - }, Promise.resolve()); + }); }; module.exports = { From b421a3857c4f97f2a848a1a4013b2444fa8e87a3 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 7 May 2024 19:52:53 +0900 Subject: [PATCH 048/151] Fix: enable commented code --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index ef4fc765..37ae32c1 100644 --- a/app.js +++ b/app.js @@ -88,4 +88,4 @@ app.set("io", startSocketServer(serverHttp)); require("./src/schedules")(app); // [Module] 택시 예상 비용 db 초기화 -// require("./src/modules/fare").initDatabase(); +require("./src/modules/fare").initDatabase(); From 3ea9766f4c8a220e46bb01fbf352e0d44597974e Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 7 May 2024 20:35:38 +0900 Subject: [PATCH 049/151] Fix: undo promise resolve --- src/modules/fare.js | 44 ++++++++++++++++---------------------------- src/services/fare.js | 4 ++-- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index d5719c5a..e3f4b02f 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -42,7 +42,7 @@ const initDatabase = async () => { !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.log( - "Naver Cloud API가 존재하지 않습니다.택시 비용 관련 기능을 사용할 수 없습니다." + "There is no credential for Naver Map. Taxi Fare functions are disabled." ); return; } @@ -52,8 +52,9 @@ const initDatabase = async () => { const location = await locationModel.find({ isValid: { $eq: true } }); location.map(async (from) => { - await location.reduce(async (acc, to) => { - await acc.then(); + location.reduce(async (acc, to) => { + logger.info(`Initializing fare from ${from.koName} to ${to.koName}`); + await acc; if (from._id === to._id) return; let tableFare = []; // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 일괄적으로 설정 @@ -76,36 +77,23 @@ const initDatabase = async () => { }); }); } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { - await axios - .get( + const fare = ( + await axios.get( `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ to.longitude + "," + to.latitude }&options=traoptimal`, { headers: naverMapApi } ) - .then((res) => { - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: res.data.route.traoptimal[0].summary.taxiFare, - isMajor: true, - }); - }); - }) - .catch((err) => { - logger.error(err.message); - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: 0, - isMajor: true, - }); - }); + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(timeConstants * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: fare, + isMajor: true, }); + }); } // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 else { @@ -143,7 +131,7 @@ const initDatabase = async () => { await taxiFareModel.insertMany(tableFare); await new Promise((resolve) => setTimeout(resolve, 200)); return acc; - }); + }, Promise.resolve()); }); } catch (err) { logger.error("Error occured while initializing database: " + err.message); diff --git a/src/services/fare.js b/src/services/fare.js index 1e0073f3..f922ad36 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -164,7 +164,7 @@ const updateTaxiFare = async (sTime, isMajor) => { const from = await locationModel.findOne({ _id: item.from }).clone(); const to = await locationModel.findOne({ _id: item.to }).clone(); - await acc.then(); + await acc; await axios .get( `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ @@ -195,7 +195,7 @@ const updateTaxiFare = async (sTime, isMajor) => { }); await new Promise((resolve) => setTimeout(() => resolve, 200)); return acc; - }); + }, Promise.resolve()); }; module.exports = { From b72bfcc2b2f7fe565c749a8bae8181884b0803e5 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 7 May 2024 20:37:49 +0900 Subject: [PATCH 050/151] Docs: comment added --- src/modules/fare.js | 2 +- src/services/fare.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index e3f4b02f..8ea2e239 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -131,7 +131,7 @@ const initDatabase = async () => { await taxiFareModel.insertMany(tableFare); await new Promise((resolve) => setTimeout(resolve, 200)); return acc; - }, Promise.resolve()); + }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 }); } catch (err) { logger.error("Error occured while initializing database: " + err.message); diff --git a/src/services/fare.js b/src/services/fare.js index f922ad36..d0cf0ada 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -195,7 +195,7 @@ const updateTaxiFare = async (sTime, isMajor) => { }); await new Promise((resolve) => setTimeout(() => resolve, 200)); return acc; - }, Promise.resolve()); + }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 }; module.exports = { From cf1a7c8168acf369e446c8b2f072a451cef010d2 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 7 May 2024 20:41:33 +0900 Subject: [PATCH 051/151] Fix: unusual case --- src/services/fare.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/fare.js b/src/services/fare.js index d0cf0ada..534db849 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -82,7 +82,7 @@ const getTaxiFare = async (req, res) => { ) .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare === 0) { + if (!taxiFare || taxiFare.fare <= 0) { await axios .get( `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ @@ -118,7 +118,7 @@ const getTaxiFare = async (req, res) => { ) .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare === 0) { + if (!taxiFare || taxiFare.fare <= 0) { await axios .get( `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ From decef9b08fc4b307d94110595abf937816a7818a Mon Sep 17 00:00:00 2001 From: static Date: Mon, 13 May 2024 00:11:39 +0900 Subject: [PATCH 052/151] Fix: dumpDB and restoreDB scripts --- package.json | 2 +- src/sampleGenerator/loadenv.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e28269bb..dfb4a606 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production node", "sample": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/index.js", "dumpDB": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/tools/dump.js", - "restoreDB": "node src/sampleGenerator/tools/restore.js" + "restoreDB": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/tools/restore.js" }, "engines": { "node": ">=18.0.0", diff --git a/src/sampleGenerator/loadenv.js b/src/sampleGenerator/loadenv.js index 0843789b..d248ee91 100644 --- a/src/sampleGenerator/loadenv.js +++ b/src/sampleGenerator/loadenv.js @@ -1,5 +1,5 @@ // Root directory에 있는 .env.test 파일을 읽어옴 -require("dotenv").config({ path: "../../.env.test" }); +require("dotenv").config({ path: "./.env.test" }); module.exports = { mongo: process.env.DB_PATH, // required From 9f72dc77b4aba223fd3498e49e427daf6904a4c5 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 13 May 2024 00:39:18 +0900 Subject: [PATCH 053/151] Add: ts-node dev dependency --- .gitignore | 1 + package.json | 7 ++-- pnpm-lock.yaml | 95 ++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index bafcba69..12d89e61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /node_modules /dist +/dump .env .env.test .env.production diff --git a/package.json b/package.json index dfb4a606..5ceb6730 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node dist/index.js", "lint": "pnpm eslint .", "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production node", - "sample": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/index.js", - "dumpDB": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/tools/dump.js", - "restoreDB": "npx tsc && tsc-alias && cross-env NODE_ENV=test node dist/sampleGenerator/tools/restore.js" + "sample": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/index.js", + "dumpDB": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/tools/dump.js", + "restoreDB": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/tools/restore.js" }, "engines": { "node": ">=18.0.0", @@ -82,6 +82,7 @@ "nodemon": "^3.0.1", "rimraf": "^5.0.5", "supertest": "^6.2.4", + "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", "typescript": "^5.2.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9202423..98dc3d4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,6 +175,9 @@ devDependencies: supertest: specifier: ^6.2.4 version: 6.3.3 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.9.0)(typescript@5.2.2) tsc-alias: specifier: ^1.8.8 version: 1.8.8 @@ -2132,6 +2135,13 @@ packages: engines: {node: '>=0.1.90'} dev: false + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + /@dabh/diagnostics@2.0.3: resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} dependencies: @@ -2523,7 +2533,6 @@ packages: /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} - dev: false /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} @@ -2543,7 +2552,6 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: false /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} @@ -2552,6 +2560,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: false + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jsdoc/salty@0.2.5: resolution: {integrity: sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==} engines: {node: '>=v12.0.0'} @@ -3666,6 +3681,22 @@ packages: dev: false optional: true + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + /@types/babel-core@6.25.7: resolution: {integrity: sha512-WPnyzNFVRo6bxpr7bcL27qXtNKNQ3iToziNBpibaXHyKGWQA0+tTLt73QQxC/5zzbM544ih6Ni5L5xrck6rGwg==} dependencies: @@ -4093,6 +4124,11 @@ packages: dependencies: acorn: 8.10.0 + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@8.10.0: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} @@ -4210,6 +4246,10 @@ packages: picomatch: 2.3.1 dev: true + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -4888,6 +4928,10 @@ packages: yaml: 1.10.2 dev: false + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + /crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} dev: false @@ -5068,6 +5112,11 @@ packages: wrappy: 1.0.2 dev: true + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + /diff@5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} @@ -7031,7 +7080,6 @@ packages: /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: false /markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2): resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} @@ -9024,6 +9072,37 @@ packages: typescript: 5.2.2 dev: true + /ts-node@10.9.2(@types/node@20.9.0)(typescript@5.2.2): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.9.0 + acorn: 8.10.0 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.2.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /tsc-alias@1.8.8: resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==} hasBin: true @@ -9220,6 +9299,7 @@ packages: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.0 + dev: true /url@0.10.3: resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} @@ -9290,6 +9370,10 @@ packages: hasBin: true dev: false + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + /v8-compile-cache@2.3.0: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true @@ -9584,6 +9668,11 @@ packages: dev: false optional: true + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} From 8804b00997f6c2e08bdb5b440ca8b43bf36487a0 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 13 May 2024 00:43:10 +0900 Subject: [PATCH 054/151] Fix: test script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ceb6730..92a1021c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "preinstall": "npx only-allow pnpm", "start": "npx tsc && tsc-alias && npx nodemon", - "mocha": "cross-env TZ='Asia/Seoul' NODE_ENV=test mocha --recursive --reporter spec --exit", + "mocha": "cross-env TZ='Asia/Seoul' NODE_ENV=test mocha --require ts-node/register --require tsconfig-paths/register --recursive --reporter spec --exit", "test": "npm run sample && cross-env TZ='Asia/Seoul' npm run mocha", "build": "tsc && tsc-alias", "clean": "rimraf dist/", From 5b8ee062fd0d79d249876358ff4594d7bf515c82 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Tue, 14 May 2024 23:24:56 +0900 Subject: [PATCH 055/151] Docs: add fare docs to swagger --- src/routes/docs/fare.js | 11 +++++++---- src/routes/docs/swaggerDocs.js | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/routes/docs/fare.js b/src/routes/docs/fare.js index 4f349bd4..72707cf1 100644 --- a/src/routes/docs/fare.js +++ b/src/routes/docs/fare.js @@ -1,4 +1,3 @@ -const { response } = require("express"); const { objectIdPattern } = require("./utils"); const tag = "fare"; @@ -30,10 +29,12 @@ fareDocs[`${apiPrefix}/getTaxiFare`] = { 200: { description: "예상 택시 요금 반환 성공", content: { - "text/plain": { + "application/json": { schema: { - type: "number", - example: 10000, + type: "object", + properties: { + fare: { type: "number", example: 10000 }, + }, }, }, }, @@ -49,3 +50,5 @@ fareDocs[`${apiPrefix}/getTaxiFare`] = { }, }, }; + +module.exports = fareDocs; diff --git a/src/routes/docs/swaggerDocs.js b/src/routes/docs/swaggerDocs.js index 62639dfa..95fd8982 100644 --- a/src/routes/docs/swaggerDocs.js +++ b/src/routes/docs/swaggerDocs.js @@ -1,5 +1,6 @@ const { reportsSchema } = require("./schemas/reportsSchema"); const { roomsSchema } = require("./schemas/roomsSchema"); +const { fareSchema } = require("./schemas/fareSchema"); const reportsDocs = require("./reports"); const logininfoDocs = require("./logininfo"); const locationsDocs = require("./locations"); @@ -8,6 +9,7 @@ const authReplaceDocs = require("./auth.replace"); const usersDocs = require("./users"); const roomsDocs = require("./rooms"); const chatsDocs = require("./chats"); +const fareDocs = require("./fare"); const { port, nodeEnv } = require("../../../loadenv"); const serverList = [ @@ -68,6 +70,10 @@ const swaggerDocs = { name: "chats", description: "채팅 시 발생하는 이벤트 정리", }, + { + name: "fare", + description: "예상 택시 금액 계산", + }, ], consumes: ["application/json"], produces: ["application/json"], @@ -80,11 +86,13 @@ const swaggerDocs = { ...authReplaceDocs, ...chatsDocs, ...roomsDocs, + ...fareDocs, }, components: { schemas: { ...reportsSchema, ...roomsSchema, + ...fareSchema, }, }, }; From 21ea16079f045899df1753f208ffcc937ec479fc Mon Sep 17 00:00:00 2001 From: static Date: Tue, 14 May 2024 23:56:05 +0900 Subject: [PATCH 056/151] Fix: test error in TypeScript environment --- src/services/users.js | 2 +- tsconfig.json | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/users.js b/src/services/users.js index 5fe9bafc..7197f01f 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,6 +1,6 @@ const { userModel } = require("@/modules/stores/mongo"); const logger = require("@/modules/logger").default; -const aws = require("@/modules/stores/aws").default; +const aws = require("@/modules/stores/aws"); const { generateNickname, generateProfileImageUrl, diff --git a/tsconfig.json b/tsconfig.json index 0c842280..09d4273e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,9 @@ } }, "include": ["src"], - "exclude": ["dist", "node_modules"] + "exclude": ["dist", "node_modules"], + "ts-node": { + "files": true + } } \ No newline at end of file From 3b7adfd4b1fe0a5f200d85374d4f92f4e91326d9 Mon Sep 17 00:00:00 2001 From: ybmin Date: Sun, 7 Jul 2024 18:58:13 +0900 Subject: [PATCH 057/151] Refactor: ts migration --- app.js | 2 +- package.json | 1 + pnpm-lock.yaml | 9 +- src/modules/{fare.js => fare.ts} | 63 ++++++-------- src/routes/fare.js | 9 -- src/routes/fare.ts | 11 +++ src/schedules/updateMajorTaxiFare.js | 14 ---- src/schedules/updateMajorTaxiFare.ts | 16 ++++ ...inorTaxiFare.js => updateMinorTaxiFare.ts} | 12 +-- src/services/{fare.js => fare.ts} | 82 +++++++++++-------- 10 files changed, 115 insertions(+), 104 deletions(-) rename src/modules/{fare.js => fare.ts} (60%) delete mode 100644 src/routes/fare.js create mode 100644 src/routes/fare.ts delete mode 100644 src/schedules/updateMajorTaxiFare.js create mode 100644 src/schedules/updateMajorTaxiFare.ts rename src/schedules/{updateMinorTaxiFare.js => updateMinorTaxiFare.ts} (59%) rename src/services/{fare.js => fare.ts} (75%) diff --git a/app.js b/app.js index 37ae32c1..4abe02ef 100644 --- a/app.js +++ b/app.js @@ -69,7 +69,7 @@ app.use("/chats", require("./src/routes/chats")); app.use("/locations", require("./src/routes/locations")); app.use("/reports", require("./src/routes/reports")); app.use("/notifications", require("./src/routes/notifications")); -app.use("/fare", require("./src/routes/fare")); +app.use("/fare", require("./src/routes/fare.ts")); // [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. app.use(require("./src/middlewares/errorHandler")); diff --git a/package.json b/package.json index 4ea2a648..ab15a691 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@adminjs/express": "^5.1.0", "@adminjs/mongoose": "^3.0.3", + "@types/express": "^4.17.21", "adminjs": "^6.8.7", "aws-sdk": "^2.1386.0", "axios": "^0.27.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0317beb7..de50fb41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@adminjs/mongoose': specifier: ^3.0.3 version: 3.0.3(adminjs@6.8.7)(mongoose@6.12.0) + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 adminjs: specifier: ^6.8.7 version: 6.8.7 @@ -3666,8 +3669,8 @@ packages: '@types/send': 0.17.1 dev: false - /@types/express@4.17.17: - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.2 '@types/express-serve-static-core': 4.17.35 @@ -5997,7 +6000,7 @@ packages: resolution: {integrity: sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==} engines: {node: '>=14'} dependencies: - '@types/express': 4.17.17 + '@types/express': 4.17.21 '@types/jsonwebtoken': 9.0.2 debug: 4.3.4 jose: 4.14.4 diff --git a/src/modules/fare.js b/src/modules/fare.ts similarity index 60% rename from src/modules/fare.js rename to src/modules/fare.ts index 8ea2e239..79334376 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.ts @@ -1,27 +1,26 @@ -const logger = require("./logger"); -const axios = require("axios"); +import axios, { AxiosRequestHeaders } from "axios"; -const { naverMapApiId, naverMapApiKey } = require("../../loadenv"); -const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); +import { naverMapApiId, naverMapApiKey } from "../../loadenv"; +import { taxiFareModel, locationModel } from "./stores/mongo"; -// Naver Cloud Platform Maps Directions 5 API Keys -const naverMapApi = { - "X-NCP-APIGW-API-KEY-ID": naverMapApiId, - "X-NCP-APIGW-API-KEY": naverMapApiKey, +interface TaxiFareInfo { + from: string; + to: string; + time: number; + fare: number; + isMajor: boolean; +} + +const naverMapApi: AxiosRequestHeaders = { + "X-NCP-APIGW-API-KEY-ID": naverMapApiId || "", + "X-NCP-APIGW-API-KEY": naverMapApiKey || "", }; const naverMapApiCall = "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; -// scaledTime에 사용하는 상수입니다. 0 ~ 47 (0:00 ~ 23:30) const timeConstants = 48; -/** - * 시간을 받아서 30분 단위로 변환해서 반환합니다. - * 요일 정보도 하나로 관리 - * @summary 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) - * @param {Date} time 변환할 시간 - */ -const scaledTime = (time) => { +const scaledTime = (time: Date): number => { return ( timeConstants * time.getDay() + time.getHours() * 2 + @@ -29,35 +28,27 @@ const scaledTime = (time) => { ); }; -/** Initialize database - * 1. Erase all previous data - * 2. Sets all taxi fare to 0 - * @summary 카이스트 본원 <-> 대전역의 경우 48 * 7개의 시간대에 대한 택시 요금을 init 시점의 비용으로 설정합니다. - * @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 fare를 init 시점의 비용으로 설정합니다. time은 0으로 설정합니다. - */ -const initDatabase = async () => { +const initDatabase = async (): Promise => { try { if ( - !naverMapApi["X-NCP-APIGW-API-KEY"] || - !naverMapApi["X-NCP-APIGW-API-KEY-ID"] + naverMapApi["X-NCP-APIGW-API-KEY"] == "" || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] == "" ) { - logger.log( + console.log( "There is no credential for Naver Map. Taxi Fare functions are disabled." ); return; } - // Remove all previous data await taxiFareModel.deleteMany({}); const location = await locationModel.find({ isValid: { $eq: true } }); location.map(async (from) => { location.reduce(async (acc, to) => { - logger.info(`Initializing fare from ${from.koName} to ${to.koName}`); + console.log(`Initializing fare from ${from.koName} to ${to.koName}`); await acc; if (from._id === to._id) return; - let tableFare = []; - // 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 일괄적으로 설정 + let tableFare: TaxiFareInfo[] = []; if (from.koName === "카이스트 본원" && to.koName === "대전역") { const fare = ( await axios.get( @@ -94,9 +85,7 @@ const initDatabase = async () => { isMajor: true, }); }); - } - // 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정 - else { + } else { await axios .get( `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ @@ -116,7 +105,7 @@ const initDatabase = async () => { }); }) .catch((err) => { - logger.error(err.message); + console.error(err.message); [...Array(7)].map((_, i) => { tableFare.push({ from: from, @@ -131,11 +120,11 @@ const initDatabase = async () => { await taxiFareModel.insertMany(tableFare); await new Promise((resolve) => setTimeout(resolve, 200)); return acc; - }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 + }, Promise.resolve()); }); } catch (err) { - logger.error("Error occured while initializing database: " + err.message); + console.error("Error occured while initializing database: " + err.message); } }; -module.exports = { scaledTime, initDatabase }; +export { scaledTime, initDatabase }; diff --git a/src/routes/fare.js b/src/routes/fare.js deleted file mode 100644 index 2d66e34a..00000000 --- a/src/routes/fare.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require("express"); -const { validateQuery } = require("../middlewares/zod"); -const { fareZod } = require("./docs/schemas/fareSchema"); -const { getTaxiFare } = require("../services/fare"); -const router = express.Router(); - -router.get("/getTaxiFare", validateQuery(fareZod.getTaxiFare), getTaxiFare); - -module.exports = router; diff --git a/src/routes/fare.ts b/src/routes/fare.ts new file mode 100644 index 00000000..157c5baa --- /dev/null +++ b/src/routes/fare.ts @@ -0,0 +1,11 @@ +import express, { Router } from "express"; + +import { validateQuery } from "../middlewares/zod"; +import { fareZod } from "./docs/schemas/fareSchema"; +import { getTaxiFare } from "../services/fare"; + +const router: Router = express.Router(); + +router.get("/getTaxiFare", validateQuery(fareZod.getTaxiFare), getTaxiFare); + +export default router; diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js deleted file mode 100644 index abaff9d9..00000000 --- a/src/schedules/updateMajorTaxiFare.js +++ /dev/null @@ -1,14 +0,0 @@ -const { scaledTime } = require("../modules/fare"); -const logger = require("../modules/logger"); -const { updateTaxiFare } = require("../services/fare"); - -/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ -module.exports = (app) => async () => { - try { - time = new Date(); - sTime = scaledTime(time); - await updateTaxiFare(sTime, true); - } catch (err) { - logger.error(err); - } -}; diff --git a/src/schedules/updateMajorTaxiFare.ts b/src/schedules/updateMajorTaxiFare.ts new file mode 100644 index 00000000..43e4ff64 --- /dev/null +++ b/src/schedules/updateMajorTaxiFare.ts @@ -0,0 +1,16 @@ +import logger from "../modules/logger"; + +import { updateTaxiFare } from "../services/fare"; +import { scaledTime } from "../modules/fare"; + +/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ +export default (app: any): any => + async (): Promise => { + try { + const time: Date = new Date(); + const sTime: number = scaledTime(time); + await updateTaxiFare(sTime, true); + } catch (err) { + logger.error(err); + } + }; diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.ts similarity index 59% rename from src/schedules/updateMinorTaxiFare.js rename to src/schedules/updateMinorTaxiFare.ts index b5201712..a88cc570 100644 --- a/src/schedules/updateMinorTaxiFare.js +++ b/src/schedules/updateMinorTaxiFare.ts @@ -1,11 +1,13 @@ -const { updateTaxiFare } = require("../services/fare"); +import logger from "../modules/logger"; + +import { updateTaxiFare } from "../services/fare"; /* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 1주일 단위로 캐싱합니다. */ -module.exports = (app) => async () => { +export default (app: any): any => async (): Promise => { try { - const date = new Date(); + const date: Date = new Date(); await updateTaxiFare(48 * date.getDay(), false); // 18:00시의 택시 요금이지만 db에는 48*(요일) + 0으로 저장됨 - } catch (err) { + } catch (err: any) { logger.error(err); } -}; +}; \ No newline at end of file diff --git a/src/services/fare.js b/src/services/fare.ts similarity index 75% rename from src/services/fare.js rename to src/services/fare.ts index 534db849..3befa515 100644 --- a/src/services/fare.js +++ b/src/services/fare.ts @@ -1,14 +1,15 @@ -const axios = require("axios"); +import axios, { AxiosRequestHeaders } from "axios"; +import { Request, Response } from "express"; +import logger from "../modules/logger"; -const { naverMapApiId, naverMapApiKey } = require("../../loadenv"); -const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); -const { scaledTime } = require("../modules/fare"); -const logger = require("../modules/logger"); +import { naverMapApiId, naverMapApiKey } from "../../loadenv"; +import { taxiFareModel, locationModel } from "../modules/stores/mongo"; +import { scaledTime } from "../modules/fare"; // Naver Cloud Platform Maps Directions 5 API Keys -const naverMapApi = { - "X-NCP-APIGW-API-KEY-ID": naverMapApiId, - "X-NCP-APIGW-API-KEY": naverMapApiKey, +const naverMapApi: AxiosRequestHeaders = { + "X-NCP-APIGW-API-KEY-ID": naverMapApiId || "", + "X-NCP-APIGW-API-KEY": naverMapApiKey || "", }; const naverMapApiCall = "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; @@ -22,13 +23,13 @@ const naverMapApiCall = * - @param {mongoose.Schema.Types.ObjectId} to - 도착지 * - @param {Date} time - 출발 시간 (ISO 8601) */ -const getTaxiFare = async (req, res) => { +const getTaxiFare = async (req: Request, res: Response): Promise => { try { if ( - !naverMapApi["X-NCP-APIGW-API-KEY"] || - !naverMapApi["X-NCP-APIGW-API-KEY-ID"] + naverMapApi["X-NCP-APIGW-API-KEY"] == "" || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] == "" ) { - logger.log( + logger.info( "There is no credential for Naver Map. Taxi Fare functions are disabled." ); res @@ -44,7 +45,7 @@ const getTaxiFare = async (req, res) => { const to = await locationModel .findOne({ _id: { $eq: req.query.to } }) .clone(); - const sTime = scaledTime(new Date(req.query.time)); + const sTime = scaledTime(new Date(req.query.time as string)); if (!from || !to) { res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); @@ -55,7 +56,7 @@ const getTaxiFare = async (req, res) => { .findOne( { from: from._id, to: to._id, time: 0 }, { isMajor: true }, - (err, docs) => { + (err: Error, docs: any) => { if (err) logger.error( "Error occured while finding TaxiFare documents: " + err.message @@ -73,7 +74,7 @@ const getTaxiFare = async (req, res) => { to: to._id, time: sTime, }, - (err, docs) => { + (err: Error, docs: any) => { if (err) logger.error( "Error occured while finding TaxiFare documents: " + err.message @@ -109,7 +110,7 @@ const getTaxiFare = async (req, res) => { to: to._id, time: 0, }, - (err, docs) => { + (err: Error, docs: any) => { if (err) logger.error( "Error occured while finding TaxiFare documents: " + err.message @@ -150,10 +151,22 @@ const getTaxiFare = async (req, res) => { * 주어진 from, to, sTime에 대한 단일 택시 요금을 업데이트합니다. * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. - * @param {Date} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) + * @param {number} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 여부 */ -const updateTaxiFare = async (sTime, isMajor) => { +const updateTaxiFare = async ( + sTime: number, + isMajor: boolean +): Promise => { + if ( + naverMapApi["X-NCP-APIGW-API-KEY"] == "" || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] == "" + ) { + logger.info( + "There is no credential for Naver Map. Taxi Fare functions are disabled." + ); + return; + } const prevFares = await taxiFareModel .find({ time: sTime, @@ -176,19 +189,21 @@ const updateTaxiFare = async (sTime, isMajor) => { logger.error(err.message); }) .then(async (res) => { - await taxiFareModel - .updateOne( - { from: item.from, to: item.to, time: sTime }, - { fare: res.data.route.traoptimal[0].summary.taxiFare }, - (err, docs) => { - if (err) - logger.error( - "Error occured while updating TaxiFare document: " + - err.message - ); - } - ) - .clone(); + if (res && res.data) { + await taxiFareModel + .updateOne( + { from: item.from, to: item.to, time: sTime }, + { fare: res.data.route.traoptimal[0].summary.taxiFare }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while updating TaxiFare document: " + + err.message + ); + } + ) + .clone(); + } }) .catch((err) => { logger.error(err.message); @@ -198,7 +213,4 @@ const updateTaxiFare = async (sTime, isMajor) => { }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 }; -module.exports = { - getTaxiFare, - updateTaxiFare, -}; +export { getTaxiFare, updateTaxiFare }; From e1b82cbef563c5b5072bcb5058826c66d4b209b9 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 9 Jul 2024 20:35:29 +0900 Subject: [PATCH 058/151] Add: non credential test case execption --- loadenv.js | 6 +- src/modules/fare.ts | 173 +++++++++++------------ src/schedules/index.js | 7 +- src/services/fare.ts | 308 ++++++++++++++++++++--------------------- 4 files changed, 251 insertions(+), 243 deletions(-) diff --git a/loadenv.js b/loadenv.js index 930c2982..a1335db2 100644 --- a/loadenv.js +++ b/loadenv.js @@ -44,6 +44,8 @@ module.exports = { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional - naverMapApiId: process.env.NAVER_MAP_API_ID, // optional - naverMapApiKey: process.env.NAVER_MAP_API_KEY, //optional + naverMap: { + naverMapApiId: process.env.NAVER_MAP_API_ID || false, // optional + naverMapApiKey: process.env.NAVER_MAP_API_KEY || false, //optional + }, }; diff --git a/src/modules/fare.ts b/src/modules/fare.ts index 79334376..71473273 100644 --- a/src/modules/fare.ts +++ b/src/modules/fare.ts @@ -1,6 +1,7 @@ import axios, { AxiosRequestHeaders } from "axios"; +import logger from "../modules/logger"; -import { naverMapApiId, naverMapApiKey } from "../../loadenv"; +import { naverMap } from "../../loadenv"; import { taxiFareModel, locationModel } from "./stores/mongo"; interface TaxiFareInfo { @@ -12,8 +13,8 @@ interface TaxiFareInfo { } const naverMapApi: AxiosRequestHeaders = { - "X-NCP-APIGW-API-KEY-ID": naverMapApiId || "", - "X-NCP-APIGW-API-KEY": naverMapApiKey || "", + "X-NCP-APIGW-API-KEY-ID": naverMap.naverMapApiId, + "X-NCP-APIGW-API-KEY": naverMap.naverMapApiKey, }; const naverMapApiCall = "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; @@ -31,99 +32,101 @@ const scaledTime = (time: Date): number => { const initDatabase = async (): Promise => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] == "" || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] == "" + naverMapApi["X-NCP-APIGW-API-KEY"] == false || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] == false ) { - console.log( + logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." ); - return; - } - await taxiFareModel.deleteMany({}); + } else { + await taxiFareModel.deleteMany({}); - const location = await locationModel.find({ isValid: { $eq: true } }); + const location = await locationModel.find({ isValid: { $eq: true } }); - location.map(async (from) => { - location.reduce(async (acc, to) => { - console.log(`Initializing fare from ${from.koName} to ${to.koName}`); - await acc; - if (from._id === to._id) return; - let tableFare: TaxiFareInfo[] = []; - if (from.koName === "카이스트 본원" && to.koName === "대전역") { - const fare = ( - await axios.get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, + location.map(async (from) => { + location.reduce(async (acc, to) => { + await acc; + if (from._id === to._id) return; + let tableFare: TaxiFareInfo[] = []; + if (from.koName === "카이스트 본원" && to.koName === "대전역") { + const fare = ( + await axios.get( + `${ + naverMapApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverMapApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(timeConstants * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: fare, + isMajor: true, + }); }); - }); - } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { - const fare = ( - await axios.get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, + } else if ( + from.koName === "대전역" && + to.koName === "카이스트 본원" + ) { + const fare = ( + await axios.get( + `${ + naverMapApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverMapApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(timeConstants * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: fare, + isMajor: true, + }); }); - }); - } else { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .then((res) => { - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * timeConstants, - fare: res.data.route.traoptimal[0].summary.taxiFare, - isMajor: false, + } else { + await axios + .get( + `${ + naverMapApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverMapApi } + ) + .then((res) => { + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * timeConstants, + fare: res.data.route.traoptimal[0].summary.taxiFare, + isMajor: false, + }); }); - }); - }) - .catch((err) => { - console.error(err.message); - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * timeConstants, - fare: 0, - isMajor: false, + }) + .catch((err) => { + logger.error(err.message); + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * timeConstants, + fare: 0, + isMajor: false, + }); }); }); - }); - } - await taxiFareModel.insertMany(tableFare); - await new Promise((resolve) => setTimeout(resolve, 200)); - return acc; - }, Promise.resolve()); - }); + } + await taxiFareModel.insertMany(tableFare); + await new Promise((resolve) => setTimeout(resolve, 200)); + return acc; + }, Promise.resolve()); + }); + } } catch (err) { - console.error("Error occured while initializing database: " + err.message); + logger.error("Error occured while initializing database: " + err.message); } }; diff --git a/src/schedules/index.js b/src/schedules/index.js index fda57d53..f4996ab0 100644 --- a/src/schedules/index.js +++ b/src/schedules/index.js @@ -1,10 +1,13 @@ const cron = require("node-cron"); +const { naverMapApiId, naverMapApiKey } = require("../../loadenv").naverMap; const registerSchedules = (app) => { cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); - cron.schedule("0,30 * * * * ", require("./updateMajorTaxiFare")(app)); - cron.schedule("0 18 * * *", require("./updateMinorTaxiFare")(app)); + if (naverMapApiId != false && naverMapApiKey != false) { + cron.schedule("0,30 * * * * ", require("./updateMajorTaxiFare")(app)); + cron.schedule("0 18 * * *", require("./updateMinorTaxiFare")(app)); + } }; module.exports = registerSchedules; diff --git a/src/services/fare.ts b/src/services/fare.ts index 3befa515..f70dd4e2 100644 --- a/src/services/fare.ts +++ b/src/services/fare.ts @@ -2,14 +2,14 @@ import axios, { AxiosRequestHeaders } from "axios"; import { Request, Response } from "express"; import logger from "../modules/logger"; -import { naverMapApiId, naverMapApiKey } from "../../loadenv"; +import { naverMap } from "../../loadenv"; import { taxiFareModel, locationModel } from "../modules/stores/mongo"; import { scaledTime } from "../modules/fare"; // Naver Cloud Platform Maps Directions 5 API Keys const naverMapApi: AxiosRequestHeaders = { - "X-NCP-APIGW-API-KEY-ID": naverMapApiId || "", - "X-NCP-APIGW-API-KEY": naverMapApiKey || "", + "X-NCP-APIGW-API-KEY-ID": naverMap.naverMapApiId, + "X-NCP-APIGW-API-KEY": naverMap.naverMapApiKey, }; const naverMapApiCall = "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; @@ -26,124 +26,123 @@ const naverMapApiCall = const getTaxiFare = async (req: Request, res: Response): Promise => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] == "" || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] == "" + naverMapApi["X-NCP-APIGW-API-KEY"] == false || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] == false ) { - logger.info( - "There is no credential for Naver Map. Taxi Fare functions are disabled." - ); res .status(503) - .json({ error: "fare/getTaxiFare: Naver Cloud API not found" }); - return; + .json({ error: "fare/getTaxiFare: Naver Map API credential not found" }); } - const from = await locationModel - .findOne({ - _id: { $eq: req.query.from }, - }) - .clone(); - const to = await locationModel - .findOne({ _id: { $eq: req.query.to } }) - .clone(); - const sTime = scaledTime(new Date(req.query.time as string)); + else{ - if (!from || !to) { - res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); - return; - } - const isMajor = ( - await taxiFareModel - .findOne( - { from: from._id, to: to._id, time: 0 }, - { isMajor: true }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while finding TaxiFare documents: " + err.message - ); - } - ) - .clone() - ).isMajor; - // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) - if (isMajor) { - const taxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: sTime, - }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while finding TaxiFare documents: " + err.message - ); - } - ) + const from = await locationModel + .findOne({ + _id: { $eq: req.query.from }, + }) .clone(); - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .then((text) => { - res - .status(200) - .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); - }) - .catch((err) => { - logger.error(err.message); - }); - } else { - res.status(200).json({ fare: taxiFare.fare }); - } - } else { - const taxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: 0, - }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while finding TaxiFare documents: " + err.message - ); - } - ) + const to = await locationModel + .findOne({ _id: { $eq: req.query.to } }) .clone(); - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } + const sTime = scaledTime(new Date(req.query.time as string)); + + if (!from || !to) { + res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); + return; + } + const isMajor = ( + await taxiFareModel + .findOne( + { from: from._id, to: to._id, time: 0 }, + { isMajor: true }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while finding Taxi Fare documents: " + err.message + ); + } ) - .then((text) => { - res - .status(200) - .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); - }) - .catch((err) => { - logger.error(err.message); - }); + .clone() + ).isMajor; + // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) + if (isMajor) { + const taxiFare = await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, + time: sTime, + }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while finding Taxi Fare documents: " + err.message + ); + } + ) + .clone(); + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + if (!taxiFare || taxiFare.fare <= 0) { + await axios + .get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } + ) + .then((text) => { + res + .status(200) + .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + }) + .catch((err) => { + logger.error(err.message); + }); + } else { + res.status(200).json({ fare: taxiFare.fare }); + } } else { - res.status(200).json({ fare: taxiFare.fare }); + const taxiFare = await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, + time: 0, + }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while finding Taxi Fare documents: " + err.message + ); + } + ) + .clone(); + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + if (!taxiFare || taxiFare.fare <= 0) { + await axios + .get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } + ) + .then((text) => { + res + .status(200) + .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + }) + .catch((err) => { + logger.error(err.message); + }); + } else { + res.status(200).json({ fare: taxiFare.fare }); + } } } } catch (err) { logger.error(err.message); res .status(500) - .json({ error: "fare/getTaxiFare: Failed to load taxi fare" }); + .json({ error: "fare/getTaxiFare: Failed to load Taxi Fare" }); } }; @@ -152,65 +151,66 @@ const getTaxiFare = async (req: Request, res: Response): Promise => { * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. * @param {number} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) - * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 여부 + * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 경로 / 이외 경로 */ const updateTaxiFare = async ( sTime: number, isMajor: boolean ): Promise => { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] == "" || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] == "" + naverMapApi["X-NCP-APIGW-API-KEY"] == false || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] == false ) { - logger.info( + logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." ); - return; } - const prevFares = await taxiFareModel - .find({ - time: sTime, - isMajor: isMajor, - }) - .clone(); - await prevFares.reduce(async (acc, item) => { - const from = await locationModel.findOne({ _id: item.from }).clone(); - const to = await locationModel.findOne({ _id: item.to }).clone(); - - await acc; - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .catch((err) => { - logger.error(err.message); + else{ + const prevFares = await taxiFareModel + .find({ + time: sTime, + isMajor: isMajor, }) - .then(async (res) => { - if (res && res.data) { - await taxiFareModel - .updateOne( - { from: item.from, to: item.to, time: sTime }, - { fare: res.data.route.traoptimal[0].summary.taxiFare }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while updating TaxiFare document: " + - err.message - ); - } - ) - .clone(); - } - }) - .catch((err) => { - logger.error(err.message); - }); - await new Promise((resolve) => setTimeout(() => resolve, 200)); - return acc; - }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 + .clone(); + await prevFares.reduce(async (acc, item) => { + const from = await locationModel.findOne({ _id: item.from }).clone(); + const to = await locationModel.findOne({ _id: item.to }).clone(); + + await acc; + await axios + .get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } + ) + .catch((err) => { + logger.error(err.message); + }) + .then(async (res) => { + if (res && res.data) { + await taxiFareModel + .updateOne( + { from: item.from, to: item.to, time: sTime }, + { fare: res.data.route.traoptimal[0].summary.taxiFare }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while updating Taxi Fare document: " + + err.message + ); + } + ) + .clone(); + } + }) + .catch((err) => { + logger.error(err.message); + }); + await new Promise((resolve) => setTimeout(() => resolve, 200)); + return acc; + }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 + } }; export { getTaxiFare, updateTaxiFare }; From f36c436035bfbd7378416bf348b86ec32e5e3c44 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 9 Jul 2024 22:10:15 +0900 Subject: [PATCH 059/151] Fix: code convention --- src/modules/fare.ts | 163 ++++++++++++++++---------------- src/schedules/index.js | 2 +- src/services/fare.ts | 205 ++++++++++++++++++++--------------------- 3 files changed, 181 insertions(+), 189 deletions(-) diff --git a/src/modules/fare.ts b/src/modules/fare.ts index 71473273..02b9b6e0 100644 --- a/src/modules/fare.ts +++ b/src/modules/fare.ts @@ -32,99 +32,94 @@ const scaledTime = (time: Date): number => { const initDatabase = async (): Promise => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] == false || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] == false + naverMapApi["X-NCP-APIGW-API-KEY"] === false || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." ); - } else { - await taxiFareModel.deleteMany({}); - - const location = await locationModel.find({ isValid: { $eq: true } }); - - location.map(async (from) => { - location.reduce(async (acc, to) => { - await acc; - if (from._id === to._id) return; - let tableFare: TaxiFareInfo[] = []; - if (from.koName === "카이스트 본원" && to.koName === "대전역") { - const fare = ( - await axios.get( - `${ - naverMapApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverMapApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, - }); + return; + } + await taxiFareModel.deleteMany({}); + const location = await locationModel.find({ isValid: { $eq: true } }); + location.map(async (from) => { + location.reduce(async (acc, to) => { + await acc; + if (from._id === to._id) return; + let tableFare: TaxiFareInfo[] = []; + if (from.koName === "카이스트 본원" && to.koName === "대전역") { + const fare = ( + await axios.get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(timeConstants * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: fare, + isMajor: true, }); - } else if ( - from.koName === "대전역" && - to.koName === "카이스트 본원" - ) { - const fare = ( - await axios.get( - `${ - naverMapApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverMapApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, - }); + }); + } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { + const fare = ( + await axios.get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; + [...Array(timeConstants * 7)].map((_, i) => { + tableFare.push({ + from: from._id, + to: to._id, + time: i, + fare: fare, + isMajor: true, }); - } else { - await axios - .get( - `${ - naverMapApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverMapApi } - ) - .then((res) => { - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * timeConstants, - fare: res.data.route.traoptimal[0].summary.taxiFare, - isMajor: false, - }); + }); + } else { + await axios + .get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } + ) + .then((res) => { + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * timeConstants, + fare: res.data.route.traoptimal[0].summary.taxiFare, + isMajor: false, }); - }) - .catch((err) => { - logger.error(err.message); - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * timeConstants, - fare: 0, - isMajor: false, - }); + }); + }) + .catch((err) => { + logger.error(err.message); + [...Array(7)].map((_, i) => { + tableFare.push({ + from: from, + to: to, + time: i * timeConstants, + fare: 0, + isMajor: false, }); }); - } - await taxiFareModel.insertMany(tableFare); - await new Promise((resolve) => setTimeout(resolve, 200)); - return acc; - }, Promise.resolve()); - }); - } + }); + } + await taxiFareModel.insertMany(tableFare); + await new Promise((resolve) => setTimeout(resolve, 200)); + return acc; + }, Promise.resolve()); + }); } catch (err) { logger.error("Error occured while initializing database: " + err.message); } diff --git a/src/schedules/index.js b/src/schedules/index.js index f4996ab0..76eeac10 100644 --- a/src/schedules/index.js +++ b/src/schedules/index.js @@ -4,7 +4,7 @@ const { naverMapApiId, naverMapApiKey } = require("../../loadenv").naverMap; const registerSchedules = (app) => { cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); - if (naverMapApiId != false && naverMapApiKey != false) { + if (naverMapApiId !== false && naverMapApiKey !== false) { cron.schedule("0,30 * * * * ", require("./updateMajorTaxiFare")(app)); cron.schedule("0 18 * * *", require("./updateMinorTaxiFare")(app)); } diff --git a/src/services/fare.ts b/src/services/fare.ts index f70dd4e2..2f432eba 100644 --- a/src/services/fare.ts +++ b/src/services/fare.ts @@ -26,116 +26,114 @@ const naverMapApiCall = const getTaxiFare = async (req: Request, res: Response): Promise => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] == false || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] == false + naverMapApi["X-NCP-APIGW-API-KEY"] === false || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false ) { res .status(503) .json({ error: "fare/getTaxiFare: Naver Map API credential not found" }); + return; } - else{ + const from = await locationModel + .findOne({ + _id: { $eq: req.query.from }, + }) + .clone(); + const to = await locationModel + .findOne({ _id: { $eq: req.query.to } }) + .clone(); + const sTime = scaledTime(new Date(req.query.time as string)); - const from = await locationModel - .findOne({ - _id: { $eq: req.query.from }, - }) - .clone(); - const to = await locationModel - .findOne({ _id: { $eq: req.query.to } }) + if (!from || !to) { + res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); + return; + } + const isMajor = ( + await taxiFareModel + .findOne( + { from: from._id, to: to._id, time: 0 }, + { isMajor: true }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while finding Taxi Fare documents: " + err.message + ); + } + ) + .clone() + ).isMajor; + // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) + if (isMajor) { + const taxiFare = await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, + time: sTime, + }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while finding Taxi Fare documents: " + err.message + ); + } + ) .clone(); - const sTime = scaledTime(new Date(req.query.time as string)); - - if (!from || !to) { - res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); - return; - } - const isMajor = ( - await taxiFareModel - .findOne( - { from: from._id, to: to._id, time: 0 }, - { isMajor: true }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + err.message - ); - } - ) - .clone() - ).isMajor; - // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) - if (isMajor) { - const taxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: sTime, - }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + err.message - ); - } + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + if (!taxiFare || taxiFare.fare <= 0) { + await axios + .get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } ) - .clone(); - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .then((text) => { - res - .status(200) - .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); - }) - .catch((err) => { - logger.error(err.message); - }); - } else { - res.status(200).json({ fare: taxiFare.fare }); - } + .then((text) => { + res + .status(200) + .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + }) + .catch((err) => { + logger.error(err.message); + }); } else { - const taxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: 0, - }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + err.message - ); - } + res.status(200).json({ fare: taxiFare.fare }); + } + } else { + const taxiFare = await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, + time: 0, + }, + (err: Error, docs: any) => { + if (err) + logger.error( + "Error occured while finding Taxi Fare documents: " + err.message + ); + } + ) + .clone(); + //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + if (!taxiFare || taxiFare.fare <= 0) { + await axios + .get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } ) - .clone(); - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .then((text) => { - res - .status(200) - .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); - }) - .catch((err) => { - logger.error(err.message); - }); - } else { - res.status(200).json({ fare: taxiFare.fare }); - } + .then((text) => { + res + .status(200) + .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + }) + .catch((err) => { + logger.error(err.message); + }); + } else { + res.status(200).json({ fare: taxiFare.fare }); } } } catch (err) { @@ -158,14 +156,14 @@ const updateTaxiFare = async ( isMajor: boolean ): Promise => { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] == false || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] == false + naverMapApi["X-NCP-APIGW-API-KEY"] === false || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." ); + return; } - else{ const prevFares = await taxiFareModel .find({ time: sTime, @@ -210,7 +208,6 @@ const updateTaxiFare = async ( await new Promise((resolve) => setTimeout(() => resolve, 200)); return acc; }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 - } }; export { getTaxiFare, updateTaxiFare }; From 57d12376c280de26f4443aba362f1b85404e6c5c Mon Sep 17 00:00:00 2001 From: ybmin Date: Thu, 18 Jul 2024 19:33:02 +0900 Subject: [PATCH 060/151] Revert: ts to js --- app.js | 2 +- src/modules/{fare.ts => fare.js} | 26 ++-- src/routes/fare.js | 11 ++ src/routes/fare.ts | 11 -- src/schedules/updateMajorTaxiFare.js | 15 ++ src/schedules/updateMajorTaxiFare.ts | 16 --- ...inorTaxiFare.ts => updateMinorTaxiFare.js} | 12 +- src/services/{fare.ts => fare.js} | 131 +++++++++--------- 8 files changed, 107 insertions(+), 117 deletions(-) rename src/modules/{fare.ts => fare.js} (86%) create mode 100644 src/routes/fare.js delete mode 100644 src/routes/fare.ts create mode 100644 src/schedules/updateMajorTaxiFare.js delete mode 100644 src/schedules/updateMajorTaxiFare.ts rename src/schedules/{updateMinorTaxiFare.ts => updateMinorTaxiFare.js} (59%) rename src/services/{fare.ts => fare.js} (68%) diff --git a/app.js b/app.js index 4abe02ef..37ae32c1 100644 --- a/app.js +++ b/app.js @@ -69,7 +69,7 @@ app.use("/chats", require("./src/routes/chats")); app.use("/locations", require("./src/routes/locations")); app.use("/reports", require("./src/routes/reports")); app.use("/notifications", require("./src/routes/notifications")); -app.use("/fare", require("./src/routes/fare.ts")); +app.use("/fare", require("./src/routes/fare")); // [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. app.use(require("./src/middlewares/errorHandler")); diff --git a/src/modules/fare.ts b/src/modules/fare.js similarity index 86% rename from src/modules/fare.ts rename to src/modules/fare.js index 02b9b6e0..5fcecb8c 100644 --- a/src/modules/fare.ts +++ b/src/modules/fare.js @@ -1,18 +1,10 @@ -import axios, { AxiosRequestHeaders } from "axios"; -import logger from "../modules/logger"; +const axios = require("axios"); +const logger = require("./logger"); -import { naverMap } from "../../loadenv"; -import { taxiFareModel, locationModel } from "./stores/mongo"; +const { naverMap } = require("../../loadenv"); +const { taxiFareModel, locationModel } = require("./stores/mongo"); -interface TaxiFareInfo { - from: string; - to: string; - time: number; - fare: number; - isMajor: boolean; -} - -const naverMapApi: AxiosRequestHeaders = { +const naverMapApi = { "X-NCP-APIGW-API-KEY-ID": naverMap.naverMapApiId, "X-NCP-APIGW-API-KEY": naverMap.naverMapApiKey, }; @@ -21,7 +13,7 @@ const naverMapApiCall = const timeConstants = 48; -const scaledTime = (time: Date): number => { +const scaledTime = (time) => { return ( timeConstants * time.getDay() + time.getHours() * 2 + @@ -29,7 +21,7 @@ const scaledTime = (time: Date): number => { ); }; -const initDatabase = async (): Promise => { +const initDatabase = async () => { try { if ( naverMapApi["X-NCP-APIGW-API-KEY"] === false || @@ -46,7 +38,7 @@ const initDatabase = async (): Promise => { location.reduce(async (acc, to) => { await acc; if (from._id === to._id) return; - let tableFare: TaxiFareInfo[] = []; + let tableFare = []; if (from.koName === "카이스트 본원" && to.koName === "대전역") { const fare = ( await axios.get( @@ -125,4 +117,4 @@ const initDatabase = async (): Promise => { } }; -export { scaledTime, initDatabase }; +module.exports = { scaledTime, initDatabase }; diff --git a/src/routes/fare.js b/src/routes/fare.js new file mode 100644 index 00000000..d4c6e2aa --- /dev/null +++ b/src/routes/fare.js @@ -0,0 +1,11 @@ +const express = require("express"); + +const { validateQuery } = require("../middlewares/zod"); +const { fareZod } = require("./docs/schemas/fareSchema"); +const { getTaxiFare } = require("../services/fare"); + +const router = express.Router(); + +router.get("/getTaxiFare", validateQuery(fareZod.getTaxiFare), getTaxiFare); + +module.exports = router; diff --git a/src/routes/fare.ts b/src/routes/fare.ts deleted file mode 100644 index 157c5baa..00000000 --- a/src/routes/fare.ts +++ /dev/null @@ -1,11 +0,0 @@ -import express, { Router } from "express"; - -import { validateQuery } from "../middlewares/zod"; -import { fareZod } from "./docs/schemas/fareSchema"; -import { getTaxiFare } from "../services/fare"; - -const router: Router = express.Router(); - -router.get("/getTaxiFare", validateQuery(fareZod.getTaxiFare), getTaxiFare); - -export default router; diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js new file mode 100644 index 00000000..57446525 --- /dev/null +++ b/src/schedules/updateMajorTaxiFare.js @@ -0,0 +1,15 @@ +const logger = require("../modules/logger"); + +const { updateTaxiFare } = require("../services/fare"); +const { scaledTime } = require("../modules/fare"); + +/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ +module.exports = (app) => async () => { + try { + const time = new Date(); + const sTime = scaledTime(time); + await updateTaxiFare(sTime, true); + } catch (err) { + logger.error(err); + } +}; diff --git a/src/schedules/updateMajorTaxiFare.ts b/src/schedules/updateMajorTaxiFare.ts deleted file mode 100644 index 43e4ff64..00000000 --- a/src/schedules/updateMajorTaxiFare.ts +++ /dev/null @@ -1,16 +0,0 @@ -import logger from "../modules/logger"; - -import { updateTaxiFare } from "../services/fare"; -import { scaledTime } from "../modules/fare"; - -/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ -export default (app: any): any => - async (): Promise => { - try { - const time: Date = new Date(); - const sTime: number = scaledTime(time); - await updateTaxiFare(sTime, true); - } catch (err) { - logger.error(err); - } - }; diff --git a/src/schedules/updateMinorTaxiFare.ts b/src/schedules/updateMinorTaxiFare.js similarity index 59% rename from src/schedules/updateMinorTaxiFare.ts rename to src/schedules/updateMinorTaxiFare.js index a88cc570..52ba53f3 100644 --- a/src/schedules/updateMinorTaxiFare.ts +++ b/src/schedules/updateMinorTaxiFare.js @@ -1,13 +1,13 @@ -import logger from "../modules/logger"; +const logger = require("../modules/logger"); -import { updateTaxiFare } from "../services/fare"; +const { updateTaxiFare } = require("../services/fare"); /* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 1주일 단위로 캐싱합니다. */ -export default (app: any): any => async (): Promise => { +module.exports = (app) => async () => { try { - const date: Date = new Date(); + const date = new Date(); await updateTaxiFare(48 * date.getDay(), false); // 18:00시의 택시 요금이지만 db에는 48*(요일) + 0으로 저장됨 - } catch (err: any) { + } catch (err) { logger.error(err); } -}; \ No newline at end of file +}; diff --git a/src/services/fare.ts b/src/services/fare.js similarity index 68% rename from src/services/fare.ts rename to src/services/fare.js index 2f432eba..b6fb10a1 100644 --- a/src/services/fare.ts +++ b/src/services/fare.js @@ -1,13 +1,12 @@ -import axios, { AxiosRequestHeaders } from "axios"; -import { Request, Response } from "express"; -import logger from "../modules/logger"; +const axios = require("axios"); +const logger = require("../modules/logger"); -import { naverMap } from "../../loadenv"; -import { taxiFareModel, locationModel } from "../modules/stores/mongo"; -import { scaledTime } from "../modules/fare"; +const { naverMap } = require("../../loadenv"); +const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); +const { scaledTime } = require("../modules/fare"); // Naver Cloud Platform Maps Directions 5 API Keys -const naverMapApi: AxiosRequestHeaders = { +const naverMapApi = { "X-NCP-APIGW-API-KEY-ID": naverMap.naverMapApiId, "X-NCP-APIGW-API-KEY": naverMap.naverMapApiKey, }; @@ -23,15 +22,15 @@ const naverMapApiCall = * - @param {mongoose.Schema.Types.ObjectId} to - 도착지 * - @param {Date} time - 출발 시간 (ISO 8601) */ -const getTaxiFare = async (req: Request, res: Response): Promise => { +const getTaxiFare = async (req, res) => { try { if ( naverMapApi["X-NCP-APIGW-API-KEY"] === false || naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false ) { - res - .status(503) - .json({ error: "fare/getTaxiFare: Naver Map API credential not found" }); + res.status(503).json({ + error: "fare/getTaxiFare: Naver Map API credential not found", + }); return; } const from = await locationModel @@ -42,7 +41,7 @@ const getTaxiFare = async (req: Request, res: Response): Promise => { const to = await locationModel .findOne({ _id: { $eq: req.query.to } }) .clone(); - const sTime = scaledTime(new Date(req.query.time as string)); + const sTime = scaledTime(new Date(req.query.time)); if (!from || !to) { res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); @@ -53,10 +52,11 @@ const getTaxiFare = async (req: Request, res: Response): Promise => { .findOne( { from: from._id, to: to._id, time: 0 }, { isMajor: true }, - (err: Error, docs: any) => { + (err, docs) => { if (err) logger.error( - "Error occured while finding Taxi Fare documents: " + err.message + "Error occured while finding Taxi Fare documents: " + + err.message ); } ) @@ -71,10 +71,11 @@ const getTaxiFare = async (req: Request, res: Response): Promise => { to: to._id, time: sTime, }, - (err: Error, docs: any) => { + (err, docs) => { if (err) logger.error( - "Error occured while finding Taxi Fare documents: " + err.message + "Error occured while finding Taxi Fare documents: " + + err.message ); } ) @@ -107,10 +108,11 @@ const getTaxiFare = async (req: Request, res: Response): Promise => { to: to._id, time: 0, }, - (err: Error, docs: any) => { + (err, docs) => { if (err) logger.error( - "Error occured while finding Taxi Fare documents: " + err.message + "Error occured while finding Taxi Fare documents: " + + err.message ); } ) @@ -151,10 +153,7 @@ const getTaxiFare = async (req: Request, res: Response): Promise => { * @param {number} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 경로 / 이외 경로 */ -const updateTaxiFare = async ( - sTime: number, - isMajor: boolean -): Promise => { +const updateTaxiFare = async (sTime, isMajor) => { if ( naverMapApi["X-NCP-APIGW-API-KEY"] === false || naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false @@ -164,50 +163,50 @@ const updateTaxiFare = async ( ); return; } - const prevFares = await taxiFareModel - .find({ - time: sTime, - isMajor: isMajor, + const prevFares = await taxiFareModel + .find({ + time: sTime, + isMajor: isMajor, + }) + .clone(); + await prevFares.reduce(async (acc, item) => { + const from = await locationModel.findOne({ _id: item.from }).clone(); + const to = await locationModel.findOne({ _id: item.to }).clone(); + + await acc; + await axios + .get( + `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ + to.longitude + "," + to.latitude + }&options=traoptimal`, + { headers: naverMapApi } + ) + .catch((err) => { + logger.error(err.message); }) - .clone(); - await prevFares.reduce(async (acc, item) => { - const from = await locationModel.findOne({ _id: item.from }).clone(); - const to = await locationModel.findOne({ _id: item.to }).clone(); - - await acc; - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .catch((err) => { - logger.error(err.message); - }) - .then(async (res) => { - if (res && res.data) { - await taxiFareModel - .updateOne( - { from: item.from, to: item.to, time: sTime }, - { fare: res.data.route.traoptimal[0].summary.taxiFare }, - (err: Error, docs: any) => { - if (err) - logger.error( - "Error occured while updating Taxi Fare document: " + - err.message - ); - } - ) - .clone(); - } - }) - .catch((err) => { - logger.error(err.message); - }); - await new Promise((resolve) => setTimeout(() => resolve, 200)); - return acc; - }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 + .then(async (res) => { + if (res && res.data) { + await taxiFareModel + .updateOne( + { from: item.from, to: item.to, time: sTime }, + { fare: res.data.route.traoptimal[0].summary.taxiFare }, + (err, docs) => { + if (err) + logger.error( + "Error occured while updating Taxi Fare document: " + + err.message + ); + } + ) + .clone(); + } + }) + .catch((err) => { + logger.error(err.message); + }); + await new Promise((resolve) => setTimeout(() => resolve, 200)); + return acc; + }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 }; -export { getTaxiFare, updateTaxiFare }; +module.exports = { getTaxiFare, updateTaxiFare }; From a69f0fd06c53359cd2faafc4b78eb9bffd94ef68 Mon Sep 17 00:00:00 2001 From: ybmin Date: Thu, 18 Jul 2024 21:50:30 +0900 Subject: [PATCH 061/151] =?UTF-8?q?Add:=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=EC=8B=9C=20=EB=B9=88=20=ED=95=84=EB=93=9C=EB=A7=8C=20=EC=B1=84?= =?UTF-8?q?=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/fare.js | 108 ++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 5fcecb8c..63ebdb2a 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -32,82 +32,80 @@ const initDatabase = async () => { ); return; } - await taxiFareModel.deleteMany({}); const location = await locationModel.find({ isValid: { $eq: true } }); location.map(async (from) => { location.reduce(async (acc, to) => { await acc; if (from._id === to._id) return; let tableFare = []; + const prevTaxiFare = await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, + }, + { fare: true } + ) + .clone().fare; + const fare = prevTaxiFare + ? prevTaxiFare + : ( + await axios.get( + `${ + naverMapApiCall + from.longitude + "," + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverMapApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; if (from.koName === "카이스트 본원" && to.koName === "대전역") { - const fare = ( - await axios.get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; [...Array(timeConstants * 7)].map((_, i) => { tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, + updateOne: { + filter: { from: from._id, to: to._id, time: i, isMajor: true }, + update: { + $setOnInsert: { + fare: fare, + }, + }, + upsert: true, + }, }); }); } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { - const fare = ( - await axios.get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; [...Array(timeConstants * 7)].map((_, i) => { tableFare.push({ - from: from._id, - to: to._id, - time: i, - fare: fare, - isMajor: true, + updateOne: { + filter: { from: from._id, to: to._id, time: i, isMajor: true }, + update: { + $setOnInsert: { + fare: fare, + }, + }, + upsert: true, + }, }); }); } else { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .then((res) => { - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, - time: i * timeConstants, - fare: res.data.route.traoptimal[0].summary.taxiFare, - isMajor: false, - }); - }); - }) - .catch((err) => { - logger.error(err.message); - [...Array(7)].map((_, i) => { - tableFare.push({ - from: from, - to: to, + [...Array(7)].map((_, i) => { + tableFare.push({ + updateOne: { + filter: { + from: from._id, + to: to._id, time: i * timeConstants, - fare: 0, isMajor: false, - }); - }); + }, + update: { + $setOnInsert: { + fare: fare, + }, + }, + upsert: true, + }, }); + }); } - await taxiFareModel.insertMany(tableFare); + await taxiFareModel.bulkWrite(tableFare); await new Promise((resolve) => setTimeout(resolve, 200)); return acc; }, Promise.resolve()); From c7a8d29a5242b54a24983a69f17cc22cf09b3de2 Mon Sep 17 00:00:00 2001 From: ybmin Date: Thu, 18 Jul 2024 22:03:38 +0900 Subject: [PATCH 062/151] Fix: undefined error --- src/modules/fare.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 63ebdb2a..fe1a2965 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -38,15 +38,17 @@ const initDatabase = async () => { await acc; if (from._id === to._id) return; let tableFare = []; - const prevTaxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - }, - { fare: true } - ) - .clone().fare; + const prevTaxiFare = ( + await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, + }, + { fare: true } + ) + .clone() + ).fare; const fare = prevTaxiFare ? prevTaxiFare : ( From ef3bc02a3f19c232aba58498525e116718601ff1 Mon Sep 17 00:00:00 2001 From: ybmin Date: Sat, 20 Jul 2024 01:35:13 +0900 Subject: [PATCH 063/151] Fix: code review --- app.js | 2 +- loadenv.js | 4 +- package.json | 1 - src/modules/fare.js | 258 ++++++++++++++++++--------- src/modules/stores/mongo.js | 4 +- src/routes/fare.js | 8 +- src/schedules/index.js | 5 +- src/schedules/updateMajorTaxiFare.js | 3 +- src/schedules/updateMinorTaxiFare.js | 2 +- src/services/fare.js | 125 +++---------- 10 files changed, 209 insertions(+), 203 deletions(-) diff --git a/app.js b/app.js index 37ae32c1..74035415 100644 --- a/app.js +++ b/app.js @@ -88,4 +88,4 @@ app.set("io", startSocketServer(serverHttp)); require("./src/schedules")(app); // [Module] 택시 예상 비용 db 초기화 -require("./src/modules/fare").initDatabase(); +require("./src/modules/fare").initializeDatabase(); diff --git a/loadenv.js b/loadenv.js index a1335db2..cbf47d88 100644 --- a/loadenv.js +++ b/loadenv.js @@ -45,7 +45,7 @@ module.exports = { }, eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional naverMap: { - naverMapApiId: process.env.NAVER_MAP_API_ID || false, // optional - naverMapApiKey: process.env.NAVER_MAP_API_KEY || false, //optional + apiId: process.env.NAVER_MAP_API_ID, // optional + apiKey: process.env.NAVER_MAP_API_KEY, //optional }, }; diff --git a/package.json b/package.json index ab15a691..4ea2a648 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "dependencies": { "@adminjs/express": "^5.1.0", "@adminjs/mongoose": "^3.0.3", - "@types/express": "^4.17.21", "adminjs": "^6.8.7", "aws-sdk": "^2.1386.0", "axios": "^0.27.2", diff --git a/src/modules/fare.js b/src/modules/fare.js index fe1a2965..a37671dd 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -5,14 +5,18 @@ const { naverMap } = require("../../loadenv"); const { taxiFareModel, locationModel } = require("./stores/mongo"); const naverMapApi = { - "X-NCP-APIGW-API-KEY-ID": naverMap.naverMapApiId, - "X-NCP-APIGW-API-KEY": naverMap.naverMapApiKey, + "X-NCP-APIGW-API-KEY-ID": naverMap.apiId, + "X-NCP-APIGW-API-KEY": naverMap.apiKey, }; -const naverMapApiCall = - "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; +// 30분 간격으로 하루를 48개의 시간대로 나누어 택시 요금을 계산합니다. const timeConstants = 48; +/** + * 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) + * @param {Date} time: 시간 + * @returns {number} scaledTime + */ const scaledTime = (time) => { return ( timeConstants * time.getDay() + @@ -21,100 +25,184 @@ const scaledTime = (time) => { ); }; -const initDatabase = async () => { +/** + * 데이터베이스를 초기화합니다. 존재하지 않는 필드가 있을때, 기존의 값으로 초기화해 놓거나, 아얘 비어있을 경우에 api를 통해 값을 받아와 초기화합니다. + * @returns + */ +const initializeDatabase = async () => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === false || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false + naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." ); return; } - const location = await locationModel.find({ isValid: { $eq: true } }); - location.map(async (from) => { - location.reduce(async (acc, to) => { - await acc; - if (from._id === to._id) return; - let tableFare = []; - const prevTaxiFare = ( - await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - }, - { fare: true } - ) - .clone() - ).fare; - const fare = prevTaxiFare - ? prevTaxiFare - : ( - await axios.get( - `${ - naverMapApiCall + from.longitude + "," + from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, - { headers: naverMapApi } - ) - ).data.route.traoptimal[0].summary.taxiFare; - if (from.koName === "카이스트 본원" && to.koName === "대전역") { - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - updateOne: { - filter: { from: from._id, to: to._id, time: i, isMajor: true }, - update: { - $setOnInsert: { - fare: fare, + const location = await locationModel + .find({ isValid: { $eq: true } }) + .lean(); + + await Promise.all( + location.map(async (from) => { + return Promise.all( + location.map(async (to) => { + if (from._id === to._id) return; + let tableFare = []; + const prevTaxiFare = ( + await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, }, - }, - upsert: true, - }, - }); - }); - } else if (from.koName === "대전역" && to.koName === "카이스트 본원") { - [...Array(timeConstants * 7)].map((_, i) => { - tableFare.push({ - updateOne: { - filter: { from: from._id, to: to._id, time: i, isMajor: true }, - update: { - $setOnInsert: { - fare: fare, + { fare: true } + ) + .clone() + ).fare; + const fare = prevTaxiFare + ? prevTaxiFare + : await callTaxiFare(from, to); + if ( + (from.koName === "카이스트 본원" && to.koName === "대전역") || + (from.koName === "대전역" && to.koName === "카이스트 본원") + ) { + [...Array(timeConstants * 7)].map((_, i) => { + tableFare.push({ + updateOne: { + filter: { + from: from._id, + to: to._id, + time: i, + isMajor: true, + }, + update: { + $setOnInsert: { + fare: fare, + }, + }, + upsert: true, }, - }, - upsert: true, - }, - }); - }); - } else { - [...Array(7)].map((_, i) => { - tableFare.push({ - updateOne: { - filter: { - from: from._id, - to: to._id, - time: i * timeConstants, - isMajor: false, - }, - update: { - $setOnInsert: { - fare: fare, + }); + }); + } else { + [...Array(7)].map((_, i) => { + tableFare.push({ + updateOne: { + filter: { + from: from._id, + to: to._id, + time: i * timeConstants, + isMajor: false, + }, + update: { + $setOnInsert: { + fare: fare, + }, + }, + upsert: true, }, - }, - upsert: true, - }, - }); - }); - } - await taxiFareModel.bulkWrite(tableFare); - await new Promise((resolve) => setTimeout(resolve, 200)); - return acc; - }, Promise.resolve()); - }); + }); + }); + } + await taxiFareModel.bulkWrite(tableFare); + await new Promise((resolve) => setTimeout(resolve, 200)); + }) + ); + }) + ); } catch (err) { logger.error("Error occured while initializing database: " + err.message); } }; -module.exports = { scaledTime, initDatabase }; +/** + * 주어진 from, to, sTime에 대한 단일 택시 요금을 업데이트합니다. + * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. + * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. + * @param {number} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) + * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 경로 / 이외 경로 + */ +const updateTaxiFare = async (sTime, isMajor) => { + if ( + naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + ) { + logger.error( + "There is no credential for Naver Map. Taxi Fare functions are disabled." + ); + return; + } + const prevFares = await taxiFareModel + .find({ + time: sTime, + isMajor: isMajor, + }) + .clone(); + await prevFares.reduce(async (acc, item) => { + const from = await locationModel.findOne({ _id: item.from }); + const to = await locationModel.findOne({ _id: item.to }); + + await acc; + await callTaxiFare + .catch((err) => { + logger.error(err.message); + }) + .then(async (fare) => { + if (fare) { + await taxiFareModel.updateOne( + { from: item.from, to: item.to, time: sTime }, + { fare: fare }, + (err, docs) => { + if (err) + logger.error( + "Error occured while updating Taxi Fare document: " + + err.message + ); + } + ); + } + }) + .catch((err) => { + logger.error(err.message); + }); + await new Promise((resolve) => setTimeout(() => resolve, 200)); + return acc; + }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 +}; + +/** + * @param {locationSchema} from : 출발지 (longitude, latitude) + * @param {locationSchema} to : 도착지 (longitude, latitude) + * @returns naver map api call을 통해 받아온 예상 택시 요금 + */ +const callTaxiFare = async (from, to) => { + if ( + naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || + naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + ) { + logger.error( + "There is no credential for Naver Map. Taxi Fare functions are disabled." + ); + return; + } + return ( + await axios.get( + `${ + "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=" + + from.longitude + + "," + + from.latitude + }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + { headers: naverMapApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; +}; + +module.exports = { + scaledTime, + initializeDatabase, + updateTaxiFare, + callTaxiFare, +}; diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 6895e8be..8f837775 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -207,8 +207,8 @@ const adminLogSchema = Schema({ const taxiFareSchema = Schema( { - from: { type: Schema.Types.ObjectId, required: true }, // 출발지 - to: { type: Schema.Types.ObjectId, required: true }, // 도착지 + from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 출발지 + to: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 도착지 isMajor: { type: Boolean, default: false }, // 카이스트 본원 <-> 대전역 경로 여부 time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) fare: { type: Number, default: false }, // 예상 택시 요금 diff --git a/src/routes/fare.js b/src/routes/fare.js index d4c6e2aa..9455495a 100644 --- a/src/routes/fare.js +++ b/src/routes/fare.js @@ -2,10 +2,14 @@ const express = require("express"); const { validateQuery } = require("../middlewares/zod"); const { fareZod } = require("./docs/schemas/fareSchema"); -const { getTaxiFare } = require("../services/fare"); +const { getTaxiFareHandler } = require("../services/fare"); const router = express.Router(); -router.get("/getTaxiFare", validateQuery(fareZod.getTaxiFare), getTaxiFare); +router.get( + "/getTaxiFare", + validateQuery(fareZod.getTaxiFare), + getTaxiFareHandler +); module.exports = router; diff --git a/src/schedules/index.js b/src/schedules/index.js index 76eeac10..23fe121e 100644 --- a/src/schedules/index.js +++ b/src/schedules/index.js @@ -1,10 +1,11 @@ const cron = require("node-cron"); -const { naverMapApiId, naverMapApiKey } = require("../../loadenv").naverMap; +const { apiId: naverMapApiId, apiKey: naverMapApiKey } = + require("../../loadenv").naverMap; const registerSchedules = (app) => { cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); - if (naverMapApiId !== false && naverMapApiKey !== false) { + if (naverMapApiId !== undefined && naverMapApiKey !== undefined) { cron.schedule("0,30 * * * * ", require("./updateMajorTaxiFare")(app)); cron.schedule("0 18 * * *", require("./updateMinorTaxiFare")(app)); } diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js index 57446525..2f0afa03 100644 --- a/src/schedules/updateMajorTaxiFare.js +++ b/src/schedules/updateMajorTaxiFare.js @@ -1,7 +1,6 @@ const logger = require("../modules/logger"); -const { updateTaxiFare } = require("../services/fare"); -const { scaledTime } = require("../modules/fare"); +const { scaledTime, updateTaxiFare } = require("../modules/fare"); /* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ module.exports = (app) => async () => { diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.js index 52ba53f3..ef1dd241 100644 --- a/src/schedules/updateMinorTaxiFare.js +++ b/src/schedules/updateMinorTaxiFare.js @@ -1,6 +1,6 @@ const logger = require("../modules/logger"); -const { updateTaxiFare } = require("../services/fare"); +const { updateTaxiFare } = require("../modules/fare"); /* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 1주일 단위로 캐싱합니다. */ module.exports = (app) => async () => { diff --git a/src/services/fare.js b/src/services/fare.js index b6fb10a1..9ad6510f 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -1,17 +1,13 @@ -const axios = require("axios"); const logger = require("../modules/logger"); const { naverMap } = require("../../loadenv"); const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); -const { scaledTime } = require("../modules/fare"); +const { scaledTime, callTaxiFare } = require("../modules/fare"); -// Naver Cloud Platform Maps Directions 5 API Keys const naverMapApi = { - "X-NCP-APIGW-API-KEY-ID": naverMap.naverMapApiId, - "X-NCP-APIGW-API-KEY": naverMap.naverMapApiKey, + "X-NCP-APIGW-API-KEY-ID": naverMap.apiId, + "X-NCP-APIGW-API-KEY": naverMap.apiKey, }; -const naverMapApiCall = - "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start="; /** * 주어진 from, to, time에 대한 택시 요금을 반환합니다. @@ -22,29 +18,27 @@ const naverMapApiCall = * - @param {mongoose.Schema.Types.ObjectId} to - 도착지 * - @param {Date} time - 출발 시간 (ISO 8601) */ -const getTaxiFare = async (req, res) => { +const getTaxiFareHandler = async (req, res) => { try { if ( naverMapApi["X-NCP-APIGW-API-KEY"] === false || naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false ) { res.status(503).json({ - error: "fare/getTaxiFare: Naver Map API credential not found", + error: "fare/getTaxiFareHandler: Naver Map API credential not found", }); return; } - const from = await locationModel - .findOne({ - _id: { $eq: req.query.from }, - }) - .clone(); - const to = await locationModel - .findOne({ _id: { $eq: req.query.to } }) - .clone(); + const from = await locationModel.findOne({ + _id: { $eq: req.query.from }, + }); + const to = await locationModel.findOne({ _id: { $eq: req.query.to } }); const sTime = scaledTime(new Date(req.query.time)); if (!from || !to) { - res.status(400).json({ error: "fare/getTaxiFare: Wrong location" }); + res + .status(400) + .json({ error: "fare/getTaxiFareHandler: Wrong location" }); return; } const isMajor = ( @@ -82,17 +76,9 @@ const getTaxiFare = async (req, res) => { .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare <= 0) { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .then((text) => { - res - .status(200) - .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + await callTaxiFare(from, to) + .then((fare) => { + res.status(200).json({ fare: fare }); }) .catch((err) => { logger.error(err.message); @@ -119,17 +105,9 @@ const getTaxiFare = async (req, res) => { .clone(); //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare <= 0) { - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .then((text) => { - res - .status(200) - .json({ fare: text.data.route.traoptimal[0].summary.taxiFare }); + await callTaxiFare(from, to) + .then((fare) => { + res.status(200).json({ fare: fare }); }) .catch((err) => { logger.error(err.message); @@ -142,71 +120,8 @@ const getTaxiFare = async (req, res) => { logger.error(err.message); res .status(500) - .json({ error: "fare/getTaxiFare: Failed to load Taxi Fare" }); + .json({ error: "fare/getTaxiFareHandler: Failed to load Taxi Fare" }); } }; -/** - * 주어진 from, to, sTime에 대한 단일 택시 요금을 업데이트합니다. - * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. - * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. - * @param {number} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) - * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 경로 / 이외 경로 - */ -const updateTaxiFare = async (sTime, isMajor) => { - if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === false || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false - ) { - logger.error( - "There is no credential for Naver Map. Taxi Fare functions are disabled." - ); - return; - } - const prevFares = await taxiFareModel - .find({ - time: sTime, - isMajor: isMajor, - }) - .clone(); - await prevFares.reduce(async (acc, item) => { - const from = await locationModel.findOne({ _id: item.from }).clone(); - const to = await locationModel.findOne({ _id: item.to }).clone(); - - await acc; - await axios - .get( - `${naverMapApiCall + from.longitude + "," + from.latitude}&goal=${ - to.longitude + "," + to.latitude - }&options=traoptimal`, - { headers: naverMapApi } - ) - .catch((err) => { - logger.error(err.message); - }) - .then(async (res) => { - if (res && res.data) { - await taxiFareModel - .updateOne( - { from: item.from, to: item.to, time: sTime }, - { fare: res.data.route.traoptimal[0].summary.taxiFare }, - (err, docs) => { - if (err) - logger.error( - "Error occured while updating Taxi Fare document: " + - err.message - ); - } - ) - .clone(); - } - }) - .catch((err) => { - logger.error(err.message); - }); - await new Promise((resolve) => setTimeout(() => resolve, 200)); - return acc; - }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 -}; - -module.exports = { getTaxiFare, updateTaxiFare }; +module.exports = { getTaxiFareHandler }; From ad541726324632f15337106b2f4665339417dce1 Mon Sep 17 00:00:00 2001 From: ybmin Date: Sat, 20 Jul 2024 01:38:24 +0900 Subject: [PATCH 064/151] Remove: unused library --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de50fb41..db4e122d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,6 @@ dependencies: '@adminjs/mongoose': specifier: ^3.0.3 version: 3.0.3(adminjs@6.8.7)(mongoose@6.12.0) - '@types/express': - specifier: ^4.17.21 - version: 4.17.21 adminjs: specifier: ^6.8.7 version: 6.8.7 From 81a37dae8775c5cabae24af60c02f434be0939b8 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 20 Jul 2024 10:31:49 +0900 Subject: [PATCH 065/151] Fix: taxiFareModel is not displayed in admin page --- src/routes/admin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/admin.js b/src/routes/admin.js index 7cd28735..e7748523 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -13,6 +13,7 @@ const { adminLogModel, deviceTokenModel, notificationOptionModel, + taxiFareModel, } = require("../modules/stores/mongo"); const { buildResource } = require("../modules/adminResource"); @@ -36,6 +37,7 @@ const resources = [ adminLogModel, deviceTokenModel, notificationOptionModel, + taxiFareModel, ] .map(buildResource()) .concat(require("../lottery").resources); From 7c48579bed6d4f2eaf34df2317368825dbd200e3 Mon Sep 17 00:00:00 2001 From: ybmin Date: Sun, 21 Jul 2024 22:00:48 +0900 Subject: [PATCH 066/151] Fix: getTaxiFare -> getTaxiFareHandler --- src/modules/fare.js | 4 ++-- src/routes/docs/fare.js | 4 ++-- src/routes/docs/schemas/fareSchema.js | 2 +- src/routes/fare.js | 2 +- src/services/fare.js | 22 +++++++++++++--------- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index a37671dd..b2da72a5 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -59,7 +59,7 @@ const initializeDatabase = async () => { }, { fare: true } ) - .clone() + .lean() ).fare; const fare = prevTaxiFare ? prevTaxiFare @@ -139,7 +139,7 @@ const updateTaxiFare = async (sTime, isMajor) => { time: sTime, isMajor: isMajor, }) - .clone(); + .lean(); await prevFares.reduce(async (acc, item) => { const from = await locationModel.findOne({ _id: item.from }); const to = await locationModel.findOne({ _id: item.to }); diff --git a/src/routes/docs/fare.js b/src/routes/docs/fare.js index 72707cf1..62fbfa50 100644 --- a/src/routes/docs/fare.js +++ b/src/routes/docs/fare.js @@ -40,10 +40,10 @@ fareDocs[`${apiPrefix}/getTaxiFare`] = { }, }, 500: { - description: "fare/getTaxiFare: Failed to load taxi fare", + description: "fare/getTaxiFareHandler: Failed to load taxi fare", content: { "text/html": { - example: "fare/getTaxiFare: Failed to load taxi fare", + example: "fare/getTaxiFareHandler: Failed to load taxi fare", }, }, }, diff --git a/src/routes/docs/schemas/fareSchema.js b/src/routes/docs/schemas/fareSchema.js index 27f63450..0812a86a 100644 --- a/src/routes/docs/schemas/fareSchema.js +++ b/src/routes/docs/schemas/fareSchema.js @@ -3,7 +3,7 @@ const { zodToSchemaObject } = require("../utils"); const { objectId } = require("../../../modules/patterns"); const fareZod = { - getTaxiFare: z.object({ + getTaxiFareHandler: z.object({ from: z.string().regex(objectId), to: z.string().regex(objectId), time: z.string().datetime(), diff --git a/src/routes/fare.js b/src/routes/fare.js index 9455495a..0cbe37c3 100644 --- a/src/routes/fare.js +++ b/src/routes/fare.js @@ -8,7 +8,7 @@ const router = express.Router(); router.get( "/getTaxiFare", - validateQuery(fareZod.getTaxiFare), + validateQuery(fareZod.getTaxiFareHandler), getTaxiFareHandler ); diff --git a/src/services/fare.js b/src/services/fare.js index 9ad6510f..55d1ad33 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -29,10 +29,14 @@ const getTaxiFareHandler = async (req, res) => { }); return; } - const from = await locationModel.findOne({ - _id: { $eq: req.query.from }, - }); - const to = await locationModel.findOne({ _id: { $eq: req.query.to } }); + const from = await locationModel + .findOne({ + _id: { $eq: req.query.from }, + }) + .lean(); + const to = await locationModel + .findOne({ _id: { $eq: req.query.to } }) + .lean(); const sTime = scaledTime(new Date(req.query.time)); if (!from || !to) { @@ -54,7 +58,7 @@ const getTaxiFareHandler = async (req, res) => { ); } ) - .clone() + .lean() ).isMajor; // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) if (isMajor) { @@ -73,8 +77,8 @@ const getTaxiFareHandler = async (req, res) => { ); } ) - .clone(); - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + .lean(); + //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare <= 0) { await callTaxiFare(from, to) .then((fare) => { @@ -102,8 +106,8 @@ const getTaxiFareHandler = async (req, res) => { ); } ) - .clone(); - //만일 cron이 아직 돌지 않은 상태의 시간대의 정보를 필요로하는 비상시의 경우 대비 + .lean(); + //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 if (!taxiFare || taxiFare.fare <= 0) { await callTaxiFare(from, to) .then((fare) => { From b679c036caf8c5e72dd05c05e4cc16287f6381d1 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Mon, 12 Aug 2024 21:40:51 +0900 Subject: [PATCH 067/151] Add: create/join feature restriction --- src/modules/ban.js | 33 +++++++++++++++++++++++++++++++++ src/services/rooms.js | 26 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/modules/ban.js diff --git a/src/modules/ban.js b/src/modules/ban.js new file mode 100644 index 00000000..3e57289a --- /dev/null +++ b/src/modules/ban.js @@ -0,0 +1,33 @@ +const logger = require("./logger"); +const { banModel } = require("./stores/mongo"); + +const getMaxValidServiceBanRecord = async (req) => { + try { + // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userOid랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 + const bans = await banModel.find({ + userId: req.userOid, + expireAt: { + $gte: req.timestamp, + }, + "services.serviceName": "service", + }); + if (bans.length > 0) { + // 가장 expireAt이 큰 정지 기록만 반환함. + const latestBan = bans.reduce( + (max, ban) => (ban.expireAt > max.expireAt ? ban : max), + bans[0] + ); + return latestBan; + } + return; + } catch (err) { + logger.error( + "Error occured while getValidServiceBanRecord: " + err.message + ); + return; + } +}; + +module.exports = { + getMaxValidServiceBanRecord, +}; diff --git a/src/services/rooms.js b/src/services/rooms.js index 218a23d5..5029b2a2 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -2,6 +2,7 @@ const { roomModel, locationModel, userModel, + banModel, } = require("../modules/stores/mongo"); const { emitChatEvent } = require("../modules/socket"); const logger = require("../modules/logger"); @@ -21,11 +22,24 @@ const eventPeriod = eventConfig && { endAt: new Date(eventConfig.period.endAt), }; const { contracts } = require("../lottery"); +const { getMaxValidServiceBanRecord } = require("../modules/ban"); const createHandler = async (req, res) => { const { name, from, to, time, maxPartLength } = req.body; try { + // 사용자가 방 생성 기능에 대하여 이용 정지 상태인지 확인합니다. + const banRecord = await getMaxValidServiceBanRecord(req); + if (banRecord != undefined) { + const formattedExpireAt = banRecord.expireAt + .toISOString() + .replace("T", " ") + .split(".")[0]; + return res.status(400).json({ + error: `Rooms/create : user ${req.userId} is temporarily restricted from creating rooms until ${formattedExpireAt}.`, + }); + } + if (from === to) { return res.status(400).json({ error: "Rooms/create : locations are same", @@ -237,6 +251,18 @@ const joinHandler = async (req, res) => { .findOne({ id: req.userId }) .populate("ongoingRoom"); + // 사용자가 방 참여 기능에 대하여 이용 정지 상태인지 확인합니다. + const banRecord = await getMaxValidServiceBanRecord(req); + if (banRecord != undefined) { + const formattedExpireAt = banRecord.expireAt + .toISOString() + .replace("T", " ") + .split(".")[0]; + return res.status(400).json({ + error: `Rooms/join : user ${req.userId} is temporarily restricted from joining rooms until ${formattedExpireAt}.`, + }); + } + // 사용자의 참여중인 진행중인 방이 5개 이상이면 오류를 반환합니다. if (user.ongoingRoom.length >= 5) { return res.status(400).json({ From 5f4a9b976b82016be9d701c4574dc81fb440ef25 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Mon, 12 Aug 2024 21:58:55 +0900 Subject: [PATCH 068/151] Add: BanSchema --- src/modules/ban.js | 2 +- src/modules/stores/mongo.js | 25 ++++++++++--------------- src/routes/docs/users.js | 28 ++++++++-------------------- 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/modules/ban.js b/src/modules/ban.js index 3e57289a..3190b533 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -9,7 +9,7 @@ const getMaxValidServiceBanRecord = async (req) => { expireAt: { $gte: req.timestamp, }, - "services.serviceName": "service", + serviceName: "service", }); if (bans.length > 0) { // 가장 expireAt이 큰 정지 기록만 반환함. diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 8f837775..6469d0d9 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -42,21 +42,16 @@ const banSchema = Schema({ type: Date, // 정지 만료 시각 required: true, }, - services: [ - { - // 정지를 당한 서비스를 기제함 - serviceName: { - type: String, - required: true, - // 필요시 이곳에 정지를 시킬 서비스를 추가함. - enum: [ - "all", // all -> 과거/미래 모든 서비스 및 이벤트 이용 제한 - "service", // service -> 방 생성/참여 제한 - "2023-fall-event", // event -> 특정 이벤트 참여 제한 - ], - }, - }, - ], + // 정지를 당한 서비스를 기제함 + serviceName: { + type: String, + required: true, + // 필요시 이곳에 정지를 시킬 서비스를 추가함. + enum: [ + "service", // service: 방 생성/참여 제한 + "2023-fall-event", // xxxx-xxxx-event: 특정 이벤트 참여 제한 + ], + }, }); const participantSchema = Schema({ diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index deaeb113..a42c4f4c 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -364,16 +364,10 @@ usersDocs[`${apiPrefix}/isBanned`] = { description: "정지 만료 시각", example: "2024-05-21 12:00", }, - services: { - type: "array", - items: { - properties: { - serviceName: { - type: "string", - description: "정지를 당한 서비스 또는 이벤트 이름", - }, - }, - }, + serviceName: { + type: "string", + description: "정지를 당한 서비스 또는 이벤트 이름", + example: "2023-fall-event", }, }, }, @@ -433,16 +427,10 @@ usersDocs[`${apiPrefix}/getBanRecord`] = { description: "정지 만료 시각", example: "2024-05-21 12:00", }, - services: { - type: "array", - items: { - properties: { - serviceName: { - type: "string", - description: "정지를 당한 서비스 또는 이벤트 이름", - }, - }, - }, + serviceName: { + type: "string", + description: "정지를 당한 서비스 또는 이벤트 이름", + example: "2023-fall-event", }, }, }, From 89cc169a1a8808a5d51ab185fc3d7cd8ceac1de0 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Mon, 12 Aug 2024 22:06:51 +0900 Subject: [PATCH 069/151] Add: Swagger docs --- src/routes/docs/rooms.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 3ac66e6b..0c9303a2 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -72,6 +72,12 @@ roomsDocs[`${apiPrefix}/create`] = { }, }, examples: { + "방 생성 기능이 정지당한 경우": { + value: { + error: + "Rooms/join : user monday is temporarily restricted from creating rooms until 2024-08-23 15:00:00.", + }, + }, "출발지와 도착지가 같음": { value: { error: "Rooms/create : locations are same", @@ -309,6 +315,12 @@ roomsDocs[`${apiPrefix}/join`] = { }, }, examples: { + "방 참여 기능이 정지당한 경우": { + value: { + error: + "Rooms/join : user monday is temporarily restricted from joining rooms until 2024-08-23 15:00:00.", + }, + }, "사용자가 참여하는 진행 중 방이 5개 이상": { value: { error: "Rooms/join : participating in too many rooms", From 2c280c88758da41ead26e519e7045113ede4b31a Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Mon, 12 Aug 2024 22:14:01 +0900 Subject: [PATCH 070/151] Refactor: unnecessary code deletion --- src/services/rooms.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/rooms.js b/src/services/rooms.js index 5029b2a2..e9beb5fb 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -2,7 +2,6 @@ const { roomModel, locationModel, userModel, - banModel, } = require("../modules/stores/mongo"); const { emitChatEvent } = require("../modules/socket"); const logger = require("../modules/logger"); From de3329f699e329b078302a881cdb53c22dab5f6d Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 13 Aug 2024 21:50:21 +0900 Subject: [PATCH 071/151] Refactor: fix error --- src/modules/fare.js | 10 +++----- src/services/fare.js | 59 +++++++++++++++----------------------------- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index b2da72a5..42d35af8 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -145,7 +145,7 @@ const updateTaxiFare = async (sTime, isMajor) => { const to = await locationModel.findOne({ _id: item.to }); await acc; - await callTaxiFare + await callTaxiFare(from, to) .catch((err) => { logger.error(err.message); }) @@ -164,6 +164,7 @@ const updateTaxiFare = async (sTime, isMajor) => { ); } }) + .clone() .catch((err) => { logger.error(err.message); }); @@ -189,12 +190,7 @@ const callTaxiFare = async (from, to) => { } return ( await axios.get( - `${ - "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=" + - from.longitude + - "," + - from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${from.longitude},${from.latitude}}&goal=${to.longitude},${to.latitude}&options=traoptimal`, { headers: naverMapApi } ) ).data.route.traoptimal[0].summary.taxiFare; diff --git a/src/services/fare.js b/src/services/fare.js index 55d1ad33..3a3718b5 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -45,41 +45,20 @@ const getTaxiFareHandler = async (req, res) => { .json({ error: "fare/getTaxiFareHandler: Wrong location" }); return; } - const isMajor = ( - await taxiFareModel - .findOne( - { from: from._id, to: to._id, time: 0 }, - { isMajor: true }, - (err, docs) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + - err.message - ); - } - ) - .lean() - ).isMajor; - // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) - if (isMajor) { - const taxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: sTime, - }, - (err, docs) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + - err.message - ); - } - ) - .lean(); + + const fare = await taxiFareModel + .findOne({ from: from._id, to: to._id, time: sTime }, (err, docs) => { + if (err) + logger.error( + "Error occured while finding Taxi Fare documents: " + err.message + ); + }) + .clone() + .lean(); + // 해당 sTime 대로 값이 존재하는 경우 (현재: 카이스트 본원 <-> 대전역) + if (fare) { //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { + if (fare.fare <= 0) { await callTaxiFare(from, to) .then((fare) => { res.status(200).json({ fare: fare }); @@ -88,15 +67,15 @@ const getTaxiFareHandler = async (req, res) => { logger.error(err.message); }); } else { - res.status(200).json({ fare: taxiFare.fare }); + res.status(200).json({ fare: fare.fare }); } } else { - const taxiFare = await taxiFareModel + const minorTaxiFare = await taxiFareModel .findOne( { from: from._id, to: to._id, - time: 0, + time: 48 * new Date(req.query.time).getDay() + 0, }, (err, docs) => { if (err) @@ -106,9 +85,11 @@ const getTaxiFareHandler = async (req, res) => { ); } ) + .clone() .lean(); + //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { + if (!minorTaxiFare || minorTaxiFare.fare <= 0) { await callTaxiFare(from, to) .then((fare) => { res.status(200).json({ fare: fare }); @@ -117,7 +98,7 @@ const getTaxiFareHandler = async (req, res) => { logger.error(err.message); }); } else { - res.status(200).json({ fare: taxiFare.fare }); + res.status(200).json({ fare: minorTaxiFare.fare }); } } } catch (err) { From efe24c612d078f8ed5f89d8e07b8094e452e38f4 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 13 Aug 2024 23:38:30 +0900 Subject: [PATCH 072/151] Fix: callback remove --- src/modules/fare.js | 10 +--------- src/services/fare.js | 28 ++++++---------------------- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 42d35af8..6b162f4c 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -153,18 +153,10 @@ const updateTaxiFare = async (sTime, isMajor) => { if (fare) { await taxiFareModel.updateOne( { from: item.from, to: item.to, time: sTime }, - { fare: fare }, - (err, docs) => { - if (err) - logger.error( - "Error occured while updating Taxi Fare document: " + - err.message - ); - } + { fare: fare } ); } }) - .clone() .catch((err) => { logger.error(err.message); }); diff --git a/src/services/fare.js b/src/services/fare.js index 3a3718b5..5150ca0b 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -47,13 +47,7 @@ const getTaxiFareHandler = async (req, res) => { } const fare = await taxiFareModel - .findOne({ from: from._id, to: to._id, time: sTime }, (err, docs) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + err.message - ); - }) - .clone() + .findOne({ from: from._id, to: to._id, time: sTime }) .lean(); // 해당 sTime 대로 값이 존재하는 경우 (현재: 카이스트 본원 <-> 대전역) if (fare) { @@ -71,21 +65,11 @@ const getTaxiFareHandler = async (req, res) => { } } else { const minorTaxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: 48 * new Date(req.query.time).getDay() + 0, - }, - (err, docs) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + - err.message - ); - } - ) - .clone() + .findOne({ + from: from._id, + to: to._id, + time: 48 * new Date(req.query.time).getDay() + 0, + }) .lean(); //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 From ef7c98c733dbb47dd0c861fc3c1e345d9addb69e Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 13 Aug 2024 23:48:00 +0900 Subject: [PATCH 073/151] Feat: double catch --- src/modules/fare.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 6b162f4c..0c577b4f 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -156,9 +156,6 @@ const updateTaxiFare = async (sTime, isMajor) => { { fare: fare } ); } - }) - .catch((err) => { - logger.error(err.message); }); await new Promise((resolve) => setTimeout(() => resolve, 200)); return acc; From 0eed8ab37e23e7c67fd29a4e349146f887905de2 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 20 Aug 2024 23:04:18 +0900 Subject: [PATCH 074/151] Feat: null safety catch --- src/modules/fare.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 0c577b4f..b86e9d9c 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -60,7 +60,7 @@ const initializeDatabase = async () => { { fare: true } ) .lean() - ).fare; + )?.fare; const fare = prevTaxiFare ? prevTaxiFare : await callTaxiFare(from, to); From 9e6af15679c3fd4c243af2aa3a094e5952b7f232 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Aug 2024 23:09:22 +0900 Subject: [PATCH 075/151] Fix: invalid location in sampleData.json --- src/sampleGenerator/sampleData.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sampleGenerator/sampleData.json b/src/sampleGenerator/sampleData.json index 546812cd..b2e84816 100644 --- a/src/sampleGenerator/sampleData.json +++ b/src/sampleGenerator/sampleData.json @@ -45,8 +45,8 @@ { "koName": "대전복합터미널", "enName": "Daejeon Terminal Complex", - "longitude": 127.350161, - "latitude": 36.362785 + "longitude": 127.436880, + "latitude": 36.349766 }, { "koName": "만년중학교", From d5c83464fab5303206b7dd39e56cbf5214e277df Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Aug 2024 23:14:24 +0900 Subject: [PATCH 076/151] Refactor: return 0 when locations are same in getTaxiFareHandler --- src/services/fare.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/services/fare.js b/src/services/fare.js index 5150ca0b..1b8d599a 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -24,11 +24,15 @@ const getTaxiFareHandler = async (req, res) => { naverMapApi["X-NCP-APIGW-API-KEY"] === false || naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false ) { - res.status(503).json({ + return res.status(503).json({ error: "fare/getTaxiFareHandler: Naver Map API credential not found", }); - return; } + + if (req.query.from === req.query.to) { + return res.status(200).json({ fare: 0 }); + } + const from = await locationModel .findOne({ _id: { $eq: req.query.from }, @@ -40,10 +44,9 @@ const getTaxiFareHandler = async (req, res) => { const sTime = scaledTime(new Date(req.query.time)); if (!from || !to) { - res + return res .status(400) .json({ error: "fare/getTaxiFareHandler: Wrong location" }); - return; } const fare = await taxiFareModel From d1efc8133326e0b292e5062bdd293cc4d8ae5a8e Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Aug 2024 23:21:09 +0900 Subject: [PATCH 077/151] Refactor: refactoring --- src/modules/fare.js | 6 +++--- src/services/fare.js | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index b86e9d9c..58132763 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -146,9 +146,6 @@ const updateTaxiFare = async (sTime, isMajor) => { await acc; await callTaxiFare(from, to) - .catch((err) => { - logger.error(err.message); - }) .then(async (fare) => { if (fare) { await taxiFareModel.updateOne( @@ -156,6 +153,9 @@ const updateTaxiFare = async (sTime, isMajor) => { { fare: fare } ); } + }) + .catch((err) => { + logger.error(err.message); }); await new Promise((resolve) => setTimeout(() => resolve, 200)); return acc; diff --git a/src/services/fare.js b/src/services/fare.js index 1b8d599a..d2bdca5b 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -29,10 +29,6 @@ const getTaxiFareHandler = async (req, res) => { }); } - if (req.query.from === req.query.to) { - return res.status(200).json({ fare: 0 }); - } - const from = await locationModel .findOne({ _id: { $eq: req.query.from }, @@ -47,6 +43,9 @@ const getTaxiFareHandler = async (req, res) => { return res .status(400) .json({ error: "fare/getTaxiFareHandler: Wrong location" }); + } else if (req.query.from === req.query.to) { + // 프론트엔드에서 예상 택시비를 숨기기 위해 0원을 반환 + return res.status(200).json({ fare: 0 }); } const fare = await taxiFareModel From bdb48898dcb729fcab466e79f650c1727c6fa240 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Aug 2024 23:44:32 +0900 Subject: [PATCH 078/151] Fix: return 200 when there are no naver map api credentials --- src/modules/fare.js | 12 ++++++------ src/services/fare.js | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/fare.js b/src/modules/fare.js index 58132763..350b8207 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -32,8 +32,8 @@ const scaledTime = (time) => { const initializeDatabase = async () => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." @@ -126,8 +126,8 @@ const initializeDatabase = async () => { */ const updateTaxiFare = async (sTime, isMajor) => { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." @@ -169,8 +169,8 @@ const updateTaxiFare = async (sTime, isMajor) => { */ const callTaxiFare = async (from, to) => { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." diff --git a/src/services/fare.js b/src/services/fare.js index d2bdca5b..638faa65 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -21,8 +21,8 @@ const naverMapApi = { const getTaxiFareHandler = async (req, res) => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === false || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { return res.status(503).json({ error: "fare/getTaxiFareHandler: Naver Map API credential not found", From ca70330a75eba9dda29ee249411d362a9ed6a7b9 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 26 Aug 2024 13:17:36 +0900 Subject: [PATCH 079/151] Add: default value for eventConfig --- loadenv.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/loadenv.js b/loadenv.js index cbf47d88..588b40e7 100644 --- a/loadenv.js +++ b/loadenv.js @@ -43,7 +43,18 @@ module.exports = { slackWebhookUrl: { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, - eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional + eventConfig: (process.env.EVENT_CONFIG && + JSON.parse(process.env.EVENT_CONFIG)) || { + mode: "2024fall", + credit: { + name: "송편", + initialAmount: 0, + }, + period: { + startAt: new Date("2024-09-07T00:00:00+09:00"), + endAt: new Date("2024-09-24T00:00:00+09:00"), + }, + }, // optional naverMap: { apiId: process.env.NAVER_MAP_API_ID, // optional apiKey: process.env.NAVER_MAP_API_KEY, //optional From 097ec2ad2db90e48322ac64e40c8bf9531c17934 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 28 Aug 2024 11:02:13 +0900 Subject: [PATCH 080/151] Add: completedQuestSchema --- src/lottery/modules/stores/mongo.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 600c99ec..fb5eb1b7 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -10,6 +10,17 @@ const integerValidator = { message: "{VALUE} is not an integer value", }; +const completedQuestSchema = Schema({ + questId: { + type: String, + required: true, + }, + completedAt: { + type: Date, + required: true, + }, +}); + const eventStatusSchema = Schema({ userId: { type: Schema.Types.ObjectId, @@ -17,7 +28,7 @@ const eventStatusSchema = Schema({ required: true, }, completedQuests: { - type: [String], + type: [completedQuestSchema], default: [], }, creditAmount: { @@ -42,17 +53,11 @@ const eventStatusSchema = Schema({ type: Boolean, default: false, }, - group: { - type: Number, - required: true, - min: 1, - validate: integerValidator, - }, // 소속된 새터반 inviter: { type: Schema.Types.ObjectId, ref: "User", }, // 이 사용자를 초대한 사용자 - isEnabledInviteUrl: { + isInvitationUrlEnabled: { type: Boolean, default: false, }, // 초대 링크 활성화 여부 From e5d7674e9f05407d02fea03ba578a3abbbb1c181 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 28 Aug 2024 15:05:13 +0900 Subject: [PATCH 081/151] Refactor: quest list --- src/lottery/modules/contracts.js | 148 +++++++----------- src/lottery/modules/stores/mongo.js | 2 +- .../routes/docs/schemas/questsSchema.js | 4 +- 3 files changed, 61 insertions(+), 93 deletions(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 72a62deb..c0ab7c31 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -13,19 +13,10 @@ const quests = buildQuests({ firstLogin: { name: "첫 발걸음", description: - "로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.", + "이벤트 참여만 해도 송편코인을 얻을 수 있다고?? 이벤트 참여에 동의하고 송편코인을 받아 보세요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstLogin.png", - reward: 50, - }, - payingAndSending: { - name: "함께하는 택시의 여정", - description: - "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 넙죽코인을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", - imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_payingAndSending.png", - reward: 150, - maxCount: 0, + reward: 200, }, firstRoomCreation: { name: "첫 방 개설", @@ -33,33 +24,33 @@ const quests = buildQuests({ "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해보세요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstRoomCreation.png", - reward: 50, + reward: 500, }, roomSharing: { - name: "너 T야? Taxi", + name: "이 택시팟은 진짜 유명한 택시팟임", description: - "방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", + "방을 공유해 친구들을 택시팟에 초대해 보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_roomSharing.png", - reward: 50, + reward: 500, isApiRequired: true, }, - paying: { - name: "정산해요 택시의 숲", + fareSettlement: { + name: "정산의 신, 신팍스", description: - "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산하기를 요청해보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", + "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산을 요청해 보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 + 버튼을 눌러 찾을 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_paying.png", - reward: 100, + reward: 2000, maxCount: 0, }, - sending: { + farePayment: { name: "송금 완료면 I am 신뢰에요", description: "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_sending.png", - reward: 50, + reward: 2000, maxCount: 0, }, nicknameChanging: { @@ -68,7 +59,7 @@ const quests = buildQuests({ "닉네임을 변경하여 자신을 표현하세요. 마이페이지수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_nicknameChanging.png", - reward: 50, + reward: 500, }, accountChanging: { name: "계좌 등록을 해야 능률이 올라갑니다", @@ -76,7 +67,7 @@ const quests = buildQuests({ "정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_accountChanging.png", - reward: 50, + reward: 500, }, adPushAgreement: { name: "Taxi의 소울메이트", @@ -84,25 +75,30 @@ const quests = buildQuests({ "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 때 알려드릴 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_adPushAgreement.png", - reward: 50, + reward: 500, }, eventSharing: { - name: "너 나랑 ㅌ태태택 (1명)", - description: - "내가 초대한 사람이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", + name: "Taxi를 아십니까", + description: "내가 초대한 사람이 이벤트에 참여하면 송편코인을 드려요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", - reward: 50, + reward: 700, maxCount: 0, }, - eventSharing5: { - name: "너 나랑 ㅌ태태택 (5명)", + dailyAttendance: { + name: "하루 한 번 Taxi!", description: - "내가 초대한 사람이 5명이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", - imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", - reward: 250, + "매일 Taxi에 접속하여 출석 체크를 하면 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요. 송편코인을 얻으려면 출석 체크 페이지에서 출석 버튼을 눌러야 해요.", + imageUrl: "", + reward: 700, maxCount: 0, + isApiRequired: true, + }, + itemPurchase: { + name: "itemPurchase", + description: "itemPurchase", + imageUrl: "", + reward: 500, }, }); @@ -111,40 +107,12 @@ const quests = buildQuests({ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} - * @usage lottery/globalState/createUserGlobalStateHandler + * @usage lottery/globalState - createUserGlobalStateHandler */ const completeFirstLoginQuest = async (userId, timestamp) => { return await completeQuest(userId, timestamp, quests.firstLogin); }; -/** - * payingAndSending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. - * @param {Object} roomObject - 방의 정보입니다. - * @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. - * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. - * @param {Date} roomObject.time - 출발 시각입니다. - * @returns {Promise} - * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitSettlementHandler, rooms - commitPaymentHandler - */ -const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { - logger.info( - `User ${userId} requested to complete payingAndSendingQuest in Room ${roomObject._id}` - ); - - if (roomObject.part.length < 2) return null; - if ( - !eventPeriod || - roomObject.time >= eventPeriod.endAt || - roomObject.time < eventPeriod.startAt - ) - return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - - return await completeQuest(userId, timestamp, quests.payingAndSending); -}; - /** * firstRoomCreation 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. @@ -158,7 +126,7 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { }; /** - * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * fareSettlement 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -169,9 +137,9 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { * @description 정산 요청이 이루어질 때마다 호출해 주세요. * @usage rooms - commitSettlementHandler */ -const completePayingQuest = async (userId, timestamp, roomObject) => { +const completeFareSettlementQuest = async (userId, timestamp, roomObject) => { logger.info( - `User ${userId} requested to complete payingQuest in Room ${roomObject._id}` + `User ${userId} requested to complete fareSettlementQuest in Room ${roomObject._id}` ); if (roomObject.part.length < 2) return null; @@ -182,11 +150,11 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.paying); + return await completeQuest(userId, timestamp, quests.fareSettlement); }; /** - * sending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * farePayment 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -197,9 +165,9 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { * @description 송금이 이루어질 때마다 호출해 주세요. * @usage rooms - commitPaymentHandler */ -const completeSendingQuest = async (userId, timestamp, roomObject) => { +const completeFarePaymentQuest = async (userId, timestamp, roomObject) => { logger.info( - `User ${userId} requested to complete sendingQuest in Room ${roomObject._id}` + `User ${userId} requested to complete farePaymentQuest in Room ${roomObject._id}` ); if (roomObject.part.length < 2) return null; @@ -210,7 +178,7 @@ const completeSendingQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.sending); + return await completeQuest(userId, timestamp, quests.farePayment); }; /** @@ -241,13 +209,13 @@ const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { }; /** - * adPushAgreementQuest 퀘스트의 완료를 요청합니다. + * adPushAgreement 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다. * @returns {Promise} * @description 알림 옵션을 변경할 때마다 호출해 주세요. - * @usage notifications/editOptionsHandler + * @usage notifications - editOptionsHandler */ const completeAdPushAgreementQuest = async ( userId, @@ -260,38 +228,36 @@ const completeAdPushAgreementQuest = async ( }; /** - * eventSharing, eventSharing5 퀘스트의 완료를 요청합니다. + * eventSharing 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} - * @description 초대 링크를 통해 사용자가 이벤트에 참여할 때마다, 초대한 사용자 및 초대받은 사용자에 대해 각각 호출해 주세요. + * @usage lottery/globalState - createUserGlobalStateHandler */ const completeEventSharingQuest = async (userId, timestamp) => { - const eventSharingResult = await completeQuest( - userId, - timestamp, - quests.eventSharing - ); - if (!eventSharingResult || eventSharingResult.questCount % 5 !== 0) - return [eventSharingResult, null]; + return await completeQuest(userId, timestamp, quests.eventSharing); +}; - const eventSharing5Result = await completeQuest( - userId, - timestamp, - quests.eventSharing5 - ); - return [eventSharingResult, eventSharing5Result]; +/** + * itemPurchase 퀘스트의 완료를 요청합니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @returns {Promise} + * @description 상품을 구입할 때마다 호출해 주세요. + */ +const completeItemPurchaseQuest = async (userId, timestamp) => { + return await completeQuest(userId, timestamp, quests.itemPurchase); }; module.exports = { quests, completeFirstLoginQuest, - completePayingAndSendingQuest, completeFirstRoomCreationQuest, - completePayingQuest, - completeSendingQuest, + completeFareSettlementQuest, + completeFarePaymentQuest, completeNicknameChangingQuest, completeAccountChangingQuest, completeAdPushAgreementQuest, completeEventSharingQuest, + completeItemPurchaseQuest, }; diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index fb5eb1b7..4fd92b65 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -158,7 +158,7 @@ const transactionSchema = Schema({ }, }); transactionSchema.set("timestamps", { - createdAt: "createAt", + createdAt: "createdAt", updatedAt: false, }); diff --git a/src/lottery/routes/docs/schemas/questsSchema.js b/src/lottery/routes/docs/schemas/questsSchema.js index 2efd11cd..0ba9b229 100644 --- a/src/lottery/routes/docs/schemas/questsSchema.js +++ b/src/lottery/routes/docs/schemas/questsSchema.js @@ -2,7 +2,9 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const questsZod = { - completeHandler: z.object({ questId: z.enum(["roomSharing"]) }), + completeHandler: z.object({ + questId: z.enum(["roomSharing", "dailyAttendance"]), + }), }; const questsSchema = zodToSchemaObject(questsZod); From 5c00f4e71d09bb24ebe691b71994b12238f7f86b Mon Sep 17 00:00:00 2001 From: static Date: Wed, 28 Aug 2024 18:30:25 +0900 Subject: [PATCH 082/151] Refactor: update globalState router for 2024 fall event --- loadenv.js | 2 +- src/lottery/routes/docs/globalState.js | 84 ++++++------ .../routes/docs/schemas/globalStateSchema.js | 1 - src/lottery/routes/globalState.js | 3 +- src/lottery/services/globalState.js | 123 +++++++++--------- 5 files changed, 100 insertions(+), 113 deletions(-) diff --git a/loadenv.js b/loadenv.js index 588b40e7..4e589543 100644 --- a/loadenv.js +++ b/loadenv.js @@ -47,7 +47,7 @@ module.exports = { JSON.parse(process.env.EVENT_CONFIG)) || { mode: "2024fall", credit: { - name: "송편", + name: "송편코인", initialAmount: 0, }, period: { diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 4af3493e..1bbf23f4 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -5,24 +5,21 @@ const globalStateDocs = {}; globalStateDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], - summary: "Frontend에서 Global state로 관리하는 정보 반환", + summary: "Frontend에서 Global State로 관리하는 정보 반환", description: - "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 가져옵니다.", + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리하는 정보를 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { type: "object", required: [ "isAgreeOnTermsOfEvent", - "isEligible", + "isBanned", "creditAmount", - "groupCreditAmount", - "completedQuests", - "group", "quests", + "completedQuests", ], properties: { isAgreeOnTermsOfEvent: { @@ -30,44 +27,19 @@ globalStateDocs[`${apiPrefix}/`] = { description: "유저의 이벤트 참여 동의 여부", example: true, }, - isEligible: { - type: "boolean", - description: "유저의 이벤트 참여 가능 여부", - example: true, - }, - creditAmount: { - type: "number", - description: "재화 개수. 0 이상입니다.", - example: 1000, - }, - groupCreditAmount: { - type: "number", - description: "소속 새터반에 소속된 유저의 전체 재화 개수", - example: 35000, - }, - completedQuests: { - type: "array", - description: - "유저가 완료한 퀘스트의 배열. 여러 번 완료할 수 있는 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.", - items: { - type: "string", - description: "Quest의 Id", - example: "QUEST ID", - }, - }, isBanned: { type: "boolean", - description: "해당 유저 제재 대상 여부", + description: "유저의 이벤트 참여 제한 여부", example: false, }, - group: { + creditAmount: { type: "number", - description: "유저의 소속 새터반", - example: 16, + description: "유저의 재화 개수. 0 이상의 정수입니다.", + example: 1000, }, quests: { type: "array", - description: "Quest의 배열", + description: "전체 퀘스트의 배열", items: { type: "object", required: [ @@ -82,7 +54,7 @@ globalStateDocs[`${apiPrefix}/`] = { properties: { id: { type: "string", - description: "Quest의 Id", + description: "퀘스트의 Id", example: "QUEST ID", }, name: { @@ -98,34 +70,54 @@ globalStateDocs[`${apiPrefix}/`] = { }, imageUrl: { type: "string", - description: "이미지 썸네일 URL", + description: "퀘스트의 썸네일 이미지 URL", example: "THUMBNAIL URL", }, reward: { type: "object", - description: "완료 보상", required: ["credit"], properties: { credit: { type: "number", - description: "완료 보상 중 재화의 개수입니다.", + description: "퀘스트의 완료 보상 중 재화의 개수", example: 100, }, }, }, maxCount: { type: "number", - description: "최대 완료 가능 횟수", + description: "퀘스트의 최대 완료 가능 횟수", example: 1, }, isApiRequired: { type: "boolean", - description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청할 수 있는지 여부`, + description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청해야 하는지의 여부`, example: false, }, }, }, }, + completedQuests: { + type: "array", + description: + "유저가 완료한 퀘스트의 배열. 여러 번 완료한 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.", + items: { + type: "object", + required: ["id", "completedAt"], + properties: { + id: { + type: "string", + description: "퀘스트의 Id", + example: "QUEST ID", + }, + completedAt: { + type: "string", + description: "퀘스트의 완료 시각", + example: "2023-01-01 00:00:00", + }, + }, + }, + }, }, }, }, @@ -137,11 +129,10 @@ globalStateDocs[`${apiPrefix}/`] = { globalStateDocs[`${apiPrefix}/create`] = { post: { tags: [`${apiPrefix}`], - summary: "Frontend에서 Global state로 관리하는 정보 생성", + summary: "Frontend에서 Global State로 관리할 정보 생성", description: - "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 생성합니다.", + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리할 정보를 생성합니다.", requestBody: { - description: "", content: { "application/json": { schema: { @@ -152,7 +143,6 @@ globalStateDocs[`${apiPrefix}/create`] = { }, responses: { 200: { - description: "", content: { "application/json": { schema: { diff --git a/src/lottery/routes/docs/schemas/globalStateSchema.js b/src/lottery/routes/docs/schemas/globalStateSchema.js index 0c3c55e6..15055525 100644 --- a/src/lottery/routes/docs/schemas/globalStateSchema.js +++ b/src/lottery/routes/docs/schemas/globalStateSchema.js @@ -6,7 +6,6 @@ const globalStateZod = { createUserGlobalStateHandler: z .object({ phoneNumber: z.string().regex(user.phoneNumber), - group: z.number().gte(1).lte(26), inviter: z.string().regex(objectId), }) .partial({ inviter: true }), diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js index 1f2b4327..c4f37d39 100644 --- a/src/lottery/routes/globalState.js +++ b/src/lottery/routes/globalState.js @@ -1,7 +1,8 @@ const express = require("express"); +const router = express.Router(); + const { validateBody } = require("../../middlewares/zod"); const { globalStateZod } = require("./docs/schemas/globalStateSchema"); -const router = express.Router(); const globalStateHandlers = require("../services/globalState"); router.get("/", globalStateHandlers.getUserGlobalStateHandler); diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 5459f851..a471f4ab 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -8,61 +8,57 @@ const { eventConfig } = require("../../../loadenv"); const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); -// 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. -const checkIsUserEligible = (user) => { - // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. - if (nodeEnv !== "production") return true; +// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다. +// +// // 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. +// const checkIsUserEligible = (user) => { +// // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. +// if (nodeEnv !== "production") return true; - const kaistId = parseInt(user?.subinfo?.kaist || "0"); - return 20240001 <= kaistId && kaistId <= 20241500; -}; +// const kaistId = parseInt(user?.subinfo?.kaist || "0"); +// return 20240001 <= kaistId && kaistId <= 20241500; +// }; const getUserGlobalStateHandler = async (req, res) => { try { const userId = isLogin(req) ? getLoginInfo(req).oid : null; - const user = userId && (await userModel.findOne({ _id: userId }).lean()); - const eventStatus = userId && (await eventStatusModel - .findOne({ userId }, "completedQuests creditAmount isBanned group") + .findOne({ userId }, "completedQuests creditAmount isBanned") .lean()); if (!eventStatus) return res.json({ isAgreeOnTermsOfEvent: false, - isEligible: checkIsUserEligible(user) || !!user?.isAdmin, // 테스트를 위해 관리자인 경우 true로 설정합니다. 하지만 관리자이더라도 이벤트에 참여할 수 없습니다. - completedQuests: [], + isBanned: false, creditAmount: 0, - group: 0, - groupCreditAmount: 0, quests, + completedQuests: [], }); // group이 eventStatus.group과 같은 사용자들의 creditAmount를 합산합니다. - const groupCreditAmount = await eventStatusModel.aggregate([ - { - $match: { - group: eventStatus.group, - }, - }, - { - $group: { - _id: null, - creditAmount: { $sum: "$creditAmount" }, - }, - }, - ]); - const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount; - if (!groupCreditAmountReal && groupCreditAmountReal !== 0) - return res - .status(500) - .json({ error: "GlobalState/ : internal server error" }); + // const groupCreditAmount = await eventStatusModel.aggregate([ + // { + // $match: { + // group: eventStatus.group, + // }, + // }, + // { + // $group: { + // _id: null, + // creditAmount: { $sum: "$creditAmount" }, + // }, + // }, + // ]); + // const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount; + // if (!groupCreditAmountReal && groupCreditAmountReal !== 0) + // return res + // .status(500) + // .json({ error: "GlobalState/ : internal server error" }); return res.json({ - isAgreeOnTermsOfEvent: true, - isEligible: true, ...eventStatus, - groupCreditAmount: groupCreditAmountReal, + isAgreeOnTermsOfEvent: true, quests, }); } catch (err) { @@ -79,61 +75,62 @@ const createUserGlobalStateHandler = async (req, res) => { if (eventStatus) return res .status(400) - .json({ error: "GlobalState/Create : already created" }); + .json({ error: "GlobalState/create : already created" }); /* Request의 inviter 필드가 설정되어 있는데, 1. 해당되는 유저가 이벤트에 참여하지 않았거나, 2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나, 3. 해당되는 유저의 초대 링크가 활성화되지 않았으면, 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ - const inviterStatus = + const inviter = req.body.inviter && - (await eventStatusModel.findOne({ _id: req.body.inviter }).lean()); + (await eventStatusModel.findById(req.body.inviter).lean()); if ( req.body.inviter && - (!inviterStatus || - inviterStatus.isBanned || - !inviterStatus.isEnabledInviteUrl) + (!inviter || inviter.isBanned || !inviter.isInvitationUrlEnabled) ) return res.status(400).json({ - error: "GlobalState/Create : inviter did not participate in the event", + error: "GlobalState/create : invalid inviter", }); - const user = await userModel.findOne({ _id: req.userOid }); + const user = await userModel.findById(req.userOid); if (!user) return res .status(500) - .json({ error: "GlobalState/Create : internal server error" }); + .json({ error: "GlobalState/create : internal server error" }); // 유저가 이벤트에 참여할 수 있는지 확인합니다. - const isEligible = checkIsUserEligible(user); - if (!isEligible) - return res.status(400).json({ - error: "GlobalState/Create : not eligible to participate in the event", - }); - - // 수집한 전화번호를 User Document에 저장합니다. - // 다른 이벤트 참여 과정에서 문제가 생길 수 있으므로, 이벤트 참여 자격이 있는 경우에만 저장합니다. - user.phoneNumber = req.body.phoneNumber; - await user.save(); + // const isEligible = checkIsUserEligible(user); + // if (!isEligible) + // return res.status(400).json({ + // error: "GlobalState/create : not eligible to participate in the event", + // }); + + // 필요한 경우 유저의 전화번호를 업데이트합니다. + if (user.phoneNumber !== req.body.phoneNumber) { + if (user.phoneNumber) { + logger.info(`Past user phone number: ${user.phoneNumber}`); + logger.info(`Update user phone number: ${req.body.phoneNumber}`); + } + + user.phoneNumber = req.body.phoneNumber; + await user.save(); + } // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, - creditAmount: eventConfig?.credit.initialAmount ?? 0, - group: req.body.group, - inviter: req.body.inviter, + creditAmount: eventConfig.credit.initialAmount ?? 0, + inviter: inviter?._id ?? undefined, }); await eventStatus.save(); + // 퀘스트를 완료 처리합니다. await contracts.completeFirstLoginQuest(req.userOid, req.timestamp); - if (req.body.inviter) { + if (inviter) { await contracts.completeEventSharingQuest(req.userOid, req.timestamp); - await contracts.completeEventSharingQuest( - inviterStatus.userId, - req.timestamp - ); + await contracts.completeEventSharingQuest(inviter.userId, req.timestamp); } return res.json({ result: true }); @@ -141,7 +138,7 @@ const createUserGlobalStateHandler = async (req, res) => { logger.error(err); res .status(500) - .json({ error: "GlobalState/Create : internal server error" }); + .json({ error: "GlobalState/create : internal server error" }); } }; From 8f4c58a4414ed7d4a8b409ed014fe4ea979c73bf Mon Sep 17 00:00:00 2001 From: static Date: Wed, 28 Aug 2024 19:54:37 +0900 Subject: [PATCH 083/151] Refactor: update invites router for 2024 fall event --- src/lottery/index.js | 2 +- src/lottery/modules/stores/mongo.js | 2 +- .../routes/docs/{invite.js => invites.js} | 25 +++---- .../{inviteSchema.js => invitesSchema.js} | 6 +- src/lottery/routes/docs/swaggerDocs.js | 10 +-- src/lottery/routes/invite.js | 20 ----- src/lottery/routes/invites.js | 21 ++++++ src/lottery/services/globalState.js | 17 +++-- src/lottery/services/invite.js | 66 ----------------- src/lottery/services/invites.js | 73 +++++++++++++++++++ 10 files changed, 126 insertions(+), 116 deletions(-) rename src/lottery/routes/docs/{invite.js => invites.js} (70%) rename src/lottery/routes/docs/schemas/{inviteSchema.js => invitesSchema.js} (67%) delete mode 100644 src/lottery/routes/invite.js create mode 100644 src/lottery/routes/invites.js delete mode 100644 src/lottery/services/invite.js create mode 100644 src/lottery/services/invites.js diff --git a/src/lottery/index.js b/src/lottery/index.js index d485dfe1..30ace7ac 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -28,7 +28,7 @@ lotteryRouter.use(require("../middlewares/originValidator")); // [Router] APIs lotteryRouter.use("/globalState", require("./routes/globalState")); -lotteryRouter.use("/invite", require("./routes/invite")); +lotteryRouter.use("/invites", require("./routes/invites")); lotteryRouter.use("/transactions", require("./routes/transactions")); lotteryRouter.use("/items", require("./routes/items")); lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 4fd92b65..1a87d98a 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -57,7 +57,7 @@ const eventStatusSchema = Schema({ type: Schema.Types.ObjectId, ref: "User", }, // 이 사용자를 초대한 사용자 - isInvitationUrlEnabled: { + isInviteUrlEnabled: { type: Boolean, default: false, }, // 초대 링크 활성화 여부 diff --git a/src/lottery/routes/docs/invite.js b/src/lottery/routes/docs/invites.js similarity index 70% rename from src/lottery/routes/docs/invite.js rename to src/lottery/routes/docs/invites.js index 3a3972da..05b0db66 100644 --- a/src/lottery/routes/docs/invite.js +++ b/src/lottery/routes/docs/invites.js @@ -1,14 +1,13 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig?.mode}/invite`; +const apiPrefix = `/events/${eventConfig?.mode}/invites`; -const inviteDocs = {}; -inviteDocs[`${apiPrefix}/search/:inviter`] = { +const invitesDocs = {}; +invitesDocs[`${apiPrefix}/search/{inviter}`] = { get: { tags: [`${apiPrefix}`], - summary: "초대자 정보 조회", - description: "초대자의 정보를 조회합니다.", + summary: "초대한 유저의 정보 반환", + description: "초대한 유저의 정보를 가져옵니다.", requestBody: { - description: "", content: { "application/json": { schema: { @@ -19,7 +18,6 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = { }, responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -28,13 +26,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = { properties: { nickname: { type: "string", - description: "초대자의 닉네임", - example: "asdf", + description: "초대한 유저의 닉네임", + example: "static", }, profileImageUrl: { type: "string", - description: "초대자의 프로필 이미지 URL", - example: "IMAGE URL", + description: "초대한 유저의 프로필 이미지 URL", + example: "PROFILE URL", }, }, }, @@ -44,14 +42,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = { }, }, }; -inviteDocs[`${apiPrefix}/create`] = { +invitesDocs[`${apiPrefix}/create`] = { post: { tags: [`${apiPrefix}`], summary: "초대 링크 생성", description: "초대 링크를 생성합니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -72,4 +69,4 @@ inviteDocs[`${apiPrefix}/create`] = { }, }; -module.exports = inviteDocs; +module.exports = invitesDocs; diff --git a/src/lottery/routes/docs/schemas/inviteSchema.js b/src/lottery/routes/docs/schemas/invitesSchema.js similarity index 67% rename from src/lottery/routes/docs/schemas/inviteSchema.js rename to src/lottery/routes/docs/schemas/invitesSchema.js index e3016557..dfc33c5c 100644 --- a/src/lottery/routes/docs/schemas/inviteSchema.js +++ b/src/lottery/routes/docs/schemas/invitesSchema.js @@ -2,12 +2,12 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const { objectId } = require("../../../../modules/patterns"); -const inviteZod = { +const invitesZod = { searchInviterHandler: z.object({ inviter: z.string().regex(objectId), }), }; -const inviteSchema = zodToSchemaObject(inviteZod); +const invitesSchema = zodToSchemaObject(invitesZod); -module.exports = { inviteSchema, inviteZod }; +module.exports = { invitesZod, invitesSchema }; diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index 38ca8298..6dcec7f5 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -1,12 +1,12 @@ const globalStateDocs = require("./globalState"); -const inviteDocs = require("./invite"); +const invitesDocs = require("./invites"); const itemsDocs = require("./items"); const publicNoticeDocs = require("./publicNotice"); const questsDocs = require("./quests"); const transactionsDocs = require("./transactions"); const { globalStateSchema } = require("./schemas/globalStateSchema"); -const { inviteSchema } = require("./schemas/inviteSchema"); +const { invitesSchema } = require("./schemas/invitesSchema"); const itemsSchema = require("./schemas/itemsSchema"); const { questsSchema } = require("./schemas/questsSchema"); @@ -20,7 +20,7 @@ const eventSwaggerDocs = { description: "이벤트 - Global State 관련 API", }, { - name: `${apiPrefix}/invite`, + name: `${apiPrefix}/invites`, description: "이벤트 - 초대 링크 관련 API", }, // 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다. @@ -44,7 +44,7 @@ const eventSwaggerDocs = { ], paths: { ...globalStateDocs, - ...inviteDocs, + ...invitesDocs, //...itemsDocs, ...publicNoticeDocs, ...questsDocs, @@ -53,7 +53,7 @@ const eventSwaggerDocs = { components: { schemas: { ...globalStateSchema, - ...inviteSchema, + ...invitesSchema, //...itemsSchema, ...questsSchema, }, diff --git a/src/lottery/routes/invite.js b/src/lottery/routes/invite.js deleted file mode 100644 index eafa09cb..00000000 --- a/src/lottery/routes/invite.js +++ /dev/null @@ -1,20 +0,0 @@ -const express = require("express"); -const { validateParams } = require("../../middlewares/zod"); -const { inviteZod } = require("./docs/schemas/inviteSchema"); -const router = express.Router(); -const inviteHandlers = require("../services/invite"); - -router.get( - "/search/:inviter", - validateParams(inviteZod.searchInviterHandler), - inviteHandlers.searchInviterHandler -); - -// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 -router.use(require("../../middlewares/auth")); -router.use(require("../middlewares/checkBanned")); -router.use(require("../middlewares/timestampValidator")); - -router.post("/create", inviteHandlers.createInviteUrlHandler); - -module.exports = router; diff --git a/src/lottery/routes/invites.js b/src/lottery/routes/invites.js new file mode 100644 index 00000000..65e4271e --- /dev/null +++ b/src/lottery/routes/invites.js @@ -0,0 +1,21 @@ +const express = require("express"); +const router = express.Router(); + +const { validateParams } = require("../../middlewares/zod"); +const { invitesZod } = require("./docs/schemas/invitesSchema"); +const invitesHandlers = require("../services/invites"); + +router.get( + "/search/:inviter", + validateParams(invitesZod.searchInviterHandler), + invitesHandlers.searchInviterHandler +); + +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 +router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/checkBanned")); +router.use(require("../middlewares/timestampValidator")); + +router.post("/create", invitesHandlers.createInviteUrlHandler); + +module.exports = router; diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index a471f4ab..662a6d67 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -82,12 +82,14 @@ const createUserGlobalStateHandler = async (req, res) => { 2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나, 3. 해당되는 유저의 초대 링크가 활성화되지 않았으면, 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ - const inviter = + const inviterStatus = req.body.inviter && (await eventStatusModel.findById(req.body.inviter).lean()); if ( req.body.inviter && - (!inviter || inviter.isBanned || !inviter.isInvitationUrlEnabled) + (!inviterStatus || + inviterStatus.isBanned || + !inviterStatus.isInviteUrlEnabled) ) return res.status(400).json({ error: "GlobalState/create : invalid inviter", @@ -120,17 +122,20 @@ const createUserGlobalStateHandler = async (req, res) => { // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, - creditAmount: eventConfig.credit.initialAmount ?? 0, - inviter: inviter?._id ?? undefined, + creditAmount: eventConfig?.credit.initialAmount ?? 0, + inviter: inviterStatus?._id ?? undefined, }); await eventStatus.save(); // 퀘스트를 완료 처리합니다. await contracts.completeFirstLoginQuest(req.userOid, req.timestamp); - if (inviter) { + if (inviterStatus) { await contracts.completeEventSharingQuest(req.userOid, req.timestamp); - await contracts.completeEventSharingQuest(inviter.userId, req.timestamp); + await contracts.completeEventSharingQuest( + inviterStatus.userId, + req.timestamp + ); } return res.json({ result: true }); diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js deleted file mode 100644 index c7871273..00000000 --- a/src/lottery/services/invite.js +++ /dev/null @@ -1,66 +0,0 @@ -const { eventStatusModel } = require("../modules/stores/mongo"); -const { userModel } = require("../../modules/stores/mongo"); -const logger = require("../../modules/logger"); - -const { eventConfig } = require("../../../loadenv"); - -const searchInviterHandler = async (req, res) => { - try { - const { inviter } = req.params; - const inviterStatus = await eventStatusModel.findOne({ _id: inviter }); - if ( - !inviterStatus || - !inviterStatus.isEnabledInviteUrl || - inviterStatus.isBanned - ) - return res.status(400).json({ error: "Invite/Search : invalid inviter" }); - - const inviterInfo = await userModel.findOne({ _id: inviterStatus.userId }); - if (!inviterInfo) - return res - .status(500) - .json({ error: "Invite/Search : internal server error" }); - - return res.json({ - nickname: inviterInfo.nickname, - profileImageUrl: inviterInfo.profileImageUrl, - }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Invite/Search : internal server error" }); - } -}; - -const createInviteUrlHandler = async (req, res) => { - try { - const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`; - - if (req.eventStatus.isEnabledInviteUrl) return res.json({ inviteUrl }); - - const eventStatus = await eventStatusModel - .findOneAndUpdate( - { - _id: req.eventStatus._id, - isEnabledInviteUrl: false, - }, - { - isEnabledInviteUrl: true, - } - ) - .lean(); - if (!eventStatus) - return res - .status(500) - .json({ error: "Invite/Create : internal server error" }); - - return res.json({ inviteUrl }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Invite/Create : internal server error" }); - } -}; - -module.exports = { - searchInviterHandler, - createInviteUrlHandler, -}; diff --git a/src/lottery/services/invites.js b/src/lottery/services/invites.js new file mode 100644 index 00000000..6479bce8 --- /dev/null +++ b/src/lottery/services/invites.js @@ -0,0 +1,73 @@ +const { eventStatusModel } = require("../modules/stores/mongo"); +const { userModel } = require("../../modules/stores/mongo"); +const logger = require("../../modules/logger"); + +const { eventConfig } = require("../../../loadenv"); + +const searchInviterHandler = async (req, res) => { + try { + /* 1. 해당되는 유저가 이벤트에 참여하지 않았거나, + 2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나, + 3. 해당되는 유저의 초대 링크가 활성화되지 않았으면, + 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ + const inviterStatus = await eventStatusModel + .findById(req.params.inviter) + .lean(); + if ( + !inviterStatus || + inviterStatus.isBanned || + !inviterStatus.isInviteUrlEnabled + ) + return res + .status(400) + .json({ error: "Invites/search : invalid inviter" }); + + // 해당되는 유저의 닉네임과 프로필 이미지를 가져옵니다. + const inviter = await userModel + .findById(inviterStatus.userId, "nickname profileImageUrl") + .lean(); + if (!inviter) + return res + .status(500) + .json({ error: "Invites/search : internal server error" }); + + return res.json(inviter); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invites/search : internal server error" }); + } +}; + +const createInviteUrlHandler = async (req, res) => { + try { + const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`; + + // 이미 초대 링크가 활성화된 경우 링크를 즉시 반환합니다. + if (req.eventStatus.isInviteUrlEnabled) return res.json({ inviteUrl }); + + // 초대 링크를 활성화합니다. + const { modifiedCount } = await eventStatusModel.updateOne( + { + _id: req.eventStatus._id, + isInviteUrlEnabled: false, + }, + { + isInviteUrlEnabled: true, + } + ); + if (modifiedCount !== 1) + return res + .status(500) + .json({ error: "Invites/create : internal server error" }); + + return res.json({ inviteUrl }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invites/create : internal server error" }); + } +}; + +module.exports = { + searchInviterHandler, + createInviteUrlHandler, +}; From 61e4b07ac66490f6181c3e676b4e81a82313fcb3 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 28 Aug 2024 20:43:24 +0900 Subject: [PATCH 084/151] Refactor: update quests router for 2024 fall event --- src/lottery/modules/quests.js | 8 ++++-- src/lottery/routes/docs/quests.js | 25 ++---------------- .../routes/docs/schemas/questsSchema.js | 2 +- src/lottery/routes/quests.js | 9 ++++--- src/lottery/services/quests.js | 26 ++++++++++++++++--- src/services/rooms.js | 14 ++-------- 6 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 04c6cd4c..4876e2d8 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -14,6 +14,7 @@ const eventPeriod = eventConfig && { }; const requiredQuestFields = ["name", "description", "imageUrl", "reward"]; + const buildQuests = (quests) => { for (const [id, quest] of Object.entries(quests)) { // quest에 필수 필드가 모두 포함되어 있는지 확인합니다. @@ -61,7 +62,7 @@ const buildQuests = (quests) => { * @param {number} quest.reward.credit - 퀘스트의 완료 보상 중 재화의 양입니다. * @param {number} quest.reward.ticket1 - 퀘스트의 완료 보상 중 일반 티켓의 개수입니다. * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. - * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다. + * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화된 경우에도 실패로 처리됩니다. */ const completeQuest = async (userId, timestamp, quest) => { try { @@ -118,7 +119,10 @@ const completeQuest = async (userId, timestamp, quest) => { ticket1Amount: quest.reward.ticket1, }, $push: { - completedQuests: quest.id, + completedQuests: { + id: quest.id, + completedAt: timestamp, + }, }, } ); diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js index 14694f3e..ea8459f1 100644 --- a/src/lottery/routes/docs/quests.js +++ b/src/lottery/routes/docs/quests.js @@ -2,24 +2,22 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/quests`; const questsDocs = {}; -questsDocs[`${apiPrefix}/complete/:questId`] = { +questsDocs[`${apiPrefix}/complete/{questId}`] = { post: { tags: [`${apiPrefix}`], summary: "퀘스트 완료 요청", description: "퀘스트의 완료를 요청합니다.", requestBody: { - description: "", content: { "application/json": { schema: { - $ref: "#/components/schemas/completeHandler", + $ref: "#/components/schemas/completeQuestHandler", }, }, }, }, responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -36,25 +34,6 @@ questsDocs[`${apiPrefix}/complete/:questId`] = { }, }, }, - 400: { - description: - "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.", - content: { - "application/json": { - schema: { - type: "object", - required: ["error"], - properties: { - error: { - type: "string", - description: "", - example: "checkBanned: banned user", - }, - }, - }, - }, - }, - }, }, }, }; diff --git a/src/lottery/routes/docs/schemas/questsSchema.js b/src/lottery/routes/docs/schemas/questsSchema.js index 0ba9b229..8daf560d 100644 --- a/src/lottery/routes/docs/schemas/questsSchema.js +++ b/src/lottery/routes/docs/schemas/questsSchema.js @@ -2,7 +2,7 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const questsZod = { - completeHandler: z.object({ + completeQuestHandler: z.object({ questId: z.enum(["roomSharing", "dailyAttendance"]), }), }; diff --git a/src/lottery/routes/quests.js b/src/lottery/routes/quests.js index 4941c8d2..e9845434 100644 --- a/src/lottery/routes/quests.js +++ b/src/lottery/routes/quests.js @@ -1,18 +1,19 @@ const express = require("express"); +const router = express.Router(); + const { validateParams } = require("../../middlewares/zod"); const { questsZod } = require("./docs/schemas/questsSchema"); -const router = express.Router(); const questsHandlers = require("../services/quests"); -// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 router.use(require("../../middlewares/auth")); router.use(require("../middlewares/checkBanned")); router.use(require("../middlewares/timestampValidator")); router.post( "/complete/:questId", - validateParams(questsZod.completeHandler), - questsHandlers.completeHandler + validateParams(questsZod.completeQuestHandler), + questsHandlers.completeQuestHandler ); module.exports = router; diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js index ae1472bc..3aa586a4 100644 --- a/src/lottery/services/quests.js +++ b/src/lottery/services/quests.js @@ -3,20 +3,38 @@ const logger = require("../../modules/logger"); const contracts = require("../modules/contracts"); -const completeHandler = async (req, res) => { +const completeQuestHandler = async (req, res) => { try { const quest = contracts.quests[req.params.questId]; if (!quest || !quest.isApiRequired) - return res.status(400).json({ error: "Quests/Complete: invalid Quest" }); + return res.status(400).json({ error: "Quests/complete: invalid quest" }); + + // 출석 체크 퀘스트는 하루에 1번만 완료하도록 제한합니다. + if (quest.id === "dailyAttendance") { + const todayMidnight = new Date(req.timestamp); + todayMidnight.setHours(0, 0, 0, 0); + + const tomorrowMidnight = new Date(todayMidnight); + tomorrowMidnight.setDate(tomorrowMidnight.getDate() + 1); + + // 오늘 완료된 dailyAttendance 퀘스트가 있는지 확인합니다. + const completedQuest = req.eventStatus.completedQuests.find( + ({ id, completedAt }) => + id === quest.id && + completedAt >= todayMidnight && + completedAt < tomorrowMidnight + ); + if (completedQuest) return res.json({ result: false }); + } const result = await completeQuest(req.userOid, req.timestamp, quest); res.json({ result: !!result }); // boolean으로 변환하기 위해 !!를 사용합니다. } catch (err) { logger.error(err); - res.status(500).json({ error: "Quests/Complete: internal server error" }); + res.status(500).json({ error: "Quests/complete: internal server error" }); } }; module.exports = { - completeHandler, + completeQuestHandler, }; diff --git a/src/services/rooms.js b/src/services/rooms.js index 218a23d5..0ef798fe 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -586,12 +586,7 @@ const commitSettlementHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completePayingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( + await contracts?.completeFareSettlementQuest( req.userOid, req.timestamp, roomObject @@ -664,12 +659,7 @@ const commitPaymentHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completeSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( + await contracts?.completeFarePaymentQuest( req.userOid, req.timestamp, roomObject From 14f13ca8593dfbd33443bd6ef81ca6f7e9439940 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 28 Aug 2024 21:26:51 +0900 Subject: [PATCH 085/151] Refactor: update transactions router for 2024 fall event --- src/lottery/modules/populates/transactions.js | 5 ++- src/lottery/modules/quests.js | 2 +- src/lottery/modules/stores/mongo.js | 6 +--- src/lottery/routes/docs/transactions.js | 35 ++++++++++++------- src/lottery/routes/transactions.js | 2 +- src/lottery/services/items.js | 6 ++-- src/lottery/services/transactions.js | 20 +++++++---- 7 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/lottery/modules/populates/transactions.js b/src/lottery/modules/populates/transactions.js index 6d965258..a09428c5 100644 --- a/src/lottery/modules/populates/transactions.js +++ b/src/lottery/modules/populates/transactions.js @@ -1,8 +1,7 @@ const transactionPopulateOption = [ { - path: "item", - select: - "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType", + path: "itemId", + select: "name imageUrl", }, ]; diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 4876e2d8..6b4b1266 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -147,7 +147,7 @@ const completeQuest = async (userId, timestamp, quest) => { amount: 0, userId, questId: quest.id, - item: ticket1._id, + itemId: ticket1._id, comment: `"${quest.name}" 퀘스트를 완료해 "${ticket1.name}" ${quest.reward.ticket1}개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 1a87d98a..02838e0d 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -144,14 +144,10 @@ const transactionSchema = Schema({ questId: { type: String, }, - item: { + itemId: { type: Schema.Types.ObjectId, ref: `${modelNamePrefix}Item`, }, - itemType: { - type: Number, - enum: [0, 1, 2, 3], - }, comment: { type: String, required: true, diff --git a/src/lottery/routes/docs/transactions.js b/src/lottery/routes/docs/transactions.js index fa78238b..a041b949 100644 --- a/src/lottery/routes/docs/transactions.js +++ b/src/lottery/routes/docs/transactions.js @@ -6,10 +6,9 @@ transactionsDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], summary: "재화 입출금 내역 반환", - description: "유저의 재화 입출금 내역을 가져옵니다.", + description: "재화 입출금 내역을 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -18,16 +17,11 @@ transactionsDocs[`${apiPrefix}/`] = { properties: { transactions: { type: "array", - description: "유저의 재화 입출금 기록의 배열", + description: "유저의 재화 입출금 내역의 배열", items: { type: "object", - required: ["_id", "type", "amount", "comment", "createAt"], + required: ["type", "amount", "comment", "createdAt"], properties: { - _id: { - type: "string", - description: "Transaction의 ObjectId", - example: "OBJECT ID", - }, type: { type: "string", description: @@ -41,18 +35,33 @@ transactionsDocs[`${apiPrefix}/`] = { }, questId: { type: "string", - description: - "Transaction과 관련된 퀘스트의 Id. 퀘스트와 관련된 Transaction인 경우에만 포함됩니다.", + description: "입출금 내역과 관련된 퀘스트의 Id", example: "QUEST ID", }, + item: { + type: "object", + required: ["name", "imageUrl"], + properties: { + name: { + type: "string", + description: "상품의 이름", + example: "랜덤 상자", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "IMAGE URL", + }, + }, + }, comment: { type: "string", description: "입출금 내역에 대한 설명", example: "랜덤 상자 구입 - 50개 차감", }, - createAt: { + createdAt: { type: "string", - description: "입출금이 일어난 시각", + description: "입출금 내역이 생성된 시각", example: "2023-01-01 00:00:00", }, }, diff --git a/src/lottery/routes/transactions.js b/src/lottery/routes/transactions.js index aee05d90..f9e375ca 100644 --- a/src/lottery/routes/transactions.js +++ b/src/lottery/routes/transactions.js @@ -1,6 +1,6 @@ const express = require("express"); - const router = express.Router(); + const transactionsHandlers = require("../services/transactions"); // 아래의 Endpoint 접근 시 로그인 필요 diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 9189bae4..df452d16 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -92,8 +92,7 @@ const getRandomItem = async (req, depth) => { type: "use", amount: 0, userId: req.userOid, - item: randomItem._id, - itemType: randomItem.itemType, + itemId: randomItem._id, comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`, }); await transaction.save(); @@ -169,8 +168,7 @@ const purchaseHandler = async (req, res) => { type: "use", amount: item.price, userId: req.userOid, - item: item._id, - itemType: item.itemType, + itemId: item._id, comment: `${eventConfig?.credit.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js index 1d920870..d76186e0 100644 --- a/src/lottery/services/transactions.js +++ b/src/lottery/services/transactions.js @@ -1,22 +1,28 @@ const { transactionModel } = require("../modules/stores/mongo"); +const { + transactionPopulateOption, +} = require("../modules/populates/transactions"); const logger = require("../../modules/logger"); -const hideItemStock = (transaction) => { - if (transaction.item) { - transaction.item.stock = transaction.item.stock > 0 ? 1 : 0; +const formatTransaction = (transaction) => { + if (transaction.itemId) { + transaction.item = transaction.itemId; + delete transaction.itemId; } - return transaction; }; const getUserTransactionsHandler = async (req, res) => { try { - // userId는 이미 Frontend에서 알고 있고, 중복되는 값이므로 제외합니다. const transactions = await transactionModel - .find({ userId: req.userOid }, "_id type amount questId comment createAt") + .find( + { userId: req.userOid }, + "type amount questId itemId comment createdAt" + ) + .populate(transactionPopulateOption) .lean(); if (transactions) res.json({ - transactions, + transactions: transactions.map(formatTransaction), }); else res.status(500).json({ error: "Transactions/ : internal server error" }); From f2f610308d3f458aeda1f1f5f44ce2a8b296d0a9 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 29 Aug 2024 20:44:50 +0900 Subject: [PATCH 086/151] Add: item leaderboard --- .../routes/docs/schemas/itemsSchema.js | 65 +++++---- src/lottery/routes/items.js | 33 +++-- src/lottery/services/items.js | 134 +++++++++++++++--- src/lottery/services/transactions.js | 1 + 4 files changed, 176 insertions(+), 57 deletions(-) diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index 80912cfb..070e5b9e 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -1,3 +1,20 @@ +const { z } = require("zod"); +const { zodToSchemaObject } = require("../../../../routes/docs/utils"); +const { objectId } = require("../../../../modules/patterns"); + +const itemsZod = { + getItemLeaderboardHandler: z.object({ + itemId: z.string().regex(objectId), + }), + purchaseItemHandler: z.object({ + itemId: z.string().regex(objectId), + }), +}; + +const itemsSchema = zodToSchemaObject(itemsZod); + +module.exports = { itemsZod, itemsSchema }; + /* Item에 대한 기본적인 프로퍼티를 갖고 있는 스키마입니다. * TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다. */ @@ -71,28 +88,28 @@ const itemWithType = { }, }; -const itemsSchema = { - item: itemWithType, - relatedItem: { - ...itemWithType, - description: - "Transaction과 관련된 아이템의 Object. 아이템과 관련된 Transaction인 경우에만 포함됩니다.", - }, - rewardItem: { - ...itemBase, - description: "랜덤박스를 구입한 경우에만 포함됩니다.", - }, - purchaseHandler: { - type: "object", - required: ["itemId"], - properties: { - itemId: { - type: "string", - pattern: "^[a-fA-F\\d]{24}$", - }, - }, - errorMessage: "validation: bad request", - }, -}; +// const itemsSchema = { +// item: itemWithType, +// relatedItem: { +// ...itemWithType, +// description: +// "Transaction과 관련된 아이템의 Object. 아이템과 관련된 Transaction인 경우에만 포함됩니다.", +// }, +// rewardItem: { +// ...itemBase, +// description: "랜덤박스를 구입한 경우에만 포함됩니다.", +// }, +// purchaseHandler: { +// type: "object", +// required: ["itemId"], +// properties: { +// itemId: { +// type: "string", +// pattern: "^[a-fA-F\\d]{24}$", +// }, +// }, +// errorMessage: "validation: bad request", +// }, +// }; -module.exports = itemsSchema; +// module.exports = itemsSchema; diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 5cdf98a8..4471796a 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -1,23 +1,26 @@ const express = require("express"); - const router = express.Router(); -// TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다. + +const { validateParams } = require("../../middlewares/zod"); +const { itemsZod } = require("./docs/schemas/itemsSchema"); const itemsHandlers = require("../services/items"); -const itemsSchema = require("./docs/schemas/itemsSchema"); -// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. -// -// router.get("/list", itemsHandlers.listHandler); +router.get("/", itemsHandlers.getItemsHandler); +router.get( + "/leaderboard/:itemId", + validateParams(itemsZod.getItemLeaderboardHandler), + itemsHandlers.getItemLeaderboardHandler +); -// // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 -// router.use(require("../../middlewares/auth")); -// router.use(require("../middlewares/checkBanned")); -// router.use(require("../middlewares/timestampValidator")); +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 +router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/checkBanned")); +router.use(require("../middlewares/timestampValidator")); -// router.post( -// "/purchase/:itemId", -// validateParams(itemsSchema.purchaseHandler), -// itemsHandlers.purchaseHandler -// ); +router.post( + "/purchase/:itemId", + validateParams(itemsZod.purchaseItemHandler), + itemsHandlers.purchaseItemHandler +); module.exports = router; diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index df452d16..64cff9fd 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -3,10 +3,122 @@ const { itemModel, transactionModel, } = require("../modules/stores/mongo"); +const { userModel } = require("../../modules/stores/mongo"); +const { isLogin, getLoginInfo } = require("../../modules/auths/login"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); +const getItemsHandler = async (req, res) => { + try { + const items = await itemModel + .find( + {}, + "_id name description imageUrl instagramStoryStickerImageUrl price isDisabled itemType" + ) + .lean(); + res.json({ items }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Items/ : internal server error" }); + } +}; + +// 유도 과정은 services/publicNotice.js 파일에 정의된 calculateProbabilityV2 함수의 주석 참조 +const calculateWinProbability = (stock, users, amount, totalAmount) => { + if (users.length <= stock) return 1; + + const base = Math.pow(1 - stock / users.length, users.length / totalAmount); + return 1 - Math.pow(base, amount); +}; + +const getItemLeaderboardHandler = async (req, res) => { + try { + // 상품 정보를 가져옵니다. + const { itemId } = req.params; + const item = await itemModel.findOne({ _id: itemId, itemType: 0 }).lean(); + if (!item) + return res + .status(400) + .json({ error: "Items/leaderboard : invalid item" }); + + // 해당 상품을 구매한 유저들의 목록을 가져옵니다. + const users = await transactionModel.aggregate([ + { + $match: { + type: "use", + itemId: item._id, + }, + }, + { + $group: { + _id: "$userId", + amount: { $sum: 1 }, + }, + }, + { + $sort: { amount: -1 }, + }, + ]); + + // 리더보드 생성을 위해 필요한 정보를 계산합니다. + const totalAmount = users.reduce((acc, user) => acc + user.amount, 0); + const rankMap = new Map( + users + .map((user) => user.amount) + .reduce((acc, amount, index) => { + if (acc.length === 0 || acc[acc.length - 1][0] !== amount) { + acc.push([amount, index + 1]); + } + return acc; + }, []) + ); + + // 리더보드를 생성합니다. + const leaderboardBase = users.map((user) => ({ + userId: user._id, + amount: user.amount, + probability: calculateWinProbability( + item.stock, + users, + user.amount, + totalAmount + ), + rank: rankMap.get(user.amount), + })); + const leaderboard = await Promise.all( + leaderboardBase + .filter((user) => user.rank <= 20) + .map(async (user) => { + const userInfo = await userModel.findById(user.userId).lean(); + return { + nickname: userInfo.nickname, + profileImageUrl: userInfo.profileImageUrl, + amount: user.amount, + probability: user.probability, + }; + }) + ); + + const userId = isLogin(req) ? getLoginInfo(req).oid : null; + const user = leaderboardBase.find( + (user) => user.userId.toString() === userId + ); + + return res.json({ + leaderboard, + amount: user?.amount, + probability: user?.probability, + rank: user?.rank, + }); + } catch (err) { + logger.error(err); + res + .status(500) + .json({ error: "Items/leaderboard : internal server error" }); + } +}; + const updateEventStatus = async ( userId, { creditDelta = 0, ticket1Delta = 0, ticket2Delta = 0 } = {} @@ -108,22 +220,7 @@ const getRandomItem = async (req, depth) => { } }; -const listHandler = async (_, res) => { - try { - const items = await itemModel - .find( - {}, - "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType" - ) - .lean(); - res.json({ items: items.map(hideItemStock) }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Items/List : internal server error" }); - } -}; - -const purchaseHandler = async (req, res) => { +const purchaseItemHandler = async (req, res) => { try { const { itemId } = req.params; const item = await itemModel.findOne({ _id: itemId }).lean(); @@ -213,6 +310,7 @@ const purchaseHandler = async (req, res) => { }; module.exports = { - listHandler, - purchaseHandler, + getItemsHandler, + getItemLeaderboardHandler, + purchaseItemHandler, }; diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js index d76186e0..aee84098 100644 --- a/src/lottery/services/transactions.js +++ b/src/lottery/services/transactions.js @@ -9,6 +9,7 @@ const formatTransaction = (transaction) => { transaction.item = transaction.itemId; delete transaction.itemId; } + return transaction; }; const getUserTransactionsHandler = async (req, res) => { From c1ea3229083f343b17e02e5535f45d3bab7bef0c Mon Sep 17 00:00:00 2001 From: static Date: Sat, 31 Aug 2024 16:48:01 +0900 Subject: [PATCH 087/151] Refactor: exclude banned user from item leaderboards --- src/lottery/services/items.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 64cff9fd..d0288da5 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -56,6 +56,19 @@ const getItemLeaderboardHandler = async (req, res) => { amount: { $sum: 1 }, }, }, + { + $lookup: { + from: eventStatusModel.collection.name, + localField: "_id", + foreignField: "userId", + as: "eventStatus", + }, + }, + { + $match: { + "eventStatus.0.isBanned": false, + }, + }, { $sort: { amount: -1 }, }, From f25c646b5bd267efc08a4ed4af63118b0b9d277a Mon Sep 17 00:00:00 2001 From: static Date: Sat, 31 Aug 2024 19:00:04 +0900 Subject: [PATCH 088/151] Add: credit random box --- src/lottery/modules/stores/mongo.js | 13 +- .../routes/docs/schemas/itemsSchema.js | 5 +- src/lottery/routes/items.js | 5 +- src/lottery/services/items.js | 319 ++++++++++-------- src/lottery/services/transactions.js | 14 +- 5 files changed, 197 insertions(+), 159 deletions(-) diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 02838e0d..ffd7a567 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -129,13 +129,13 @@ const transactionSchema = Schema({ type: String, enum: ["get", "use"], required: true, - }, + }, // get: 재화 획득, use: 재화 사용 amount: { type: Number, required: true, min: 0, validate: integerValidator, - }, + }, // 재화의 변화량의 절댓값 userId: { type: Schema.Types.ObjectId, ref: "User", @@ -143,11 +143,16 @@ const transactionSchema = Schema({ }, questId: { type: String, - }, + }, // 완료한 퀘스트의 ID itemId: { type: Schema.Types.ObjectId, ref: `${modelNamePrefix}Item`, - }, + }, // 획득한 상품의 ID + itemAmount: { + type: Number, + min: 1, + validate: integerValidator, + }, // 획득한 상품의 개수 comment: { type: String, required: true, diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index 070e5b9e..fda71fcc 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -6,9 +6,12 @@ const itemsZod = { getItemLeaderboardHandler: z.object({ itemId: z.string().regex(objectId), }), - purchaseItemHandler: z.object({ + purchaseItemHandlerParams: z.object({ itemId: z.string().regex(objectId), }), + purchaseItemHandlerBody: z.object({ + amount: z.number().int().positive(), + }), }; const itemsSchema = zodToSchemaObject(itemsZod); diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 4471796a..135617b8 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const { validateParams } = require("../../middlewares/zod"); +const { validateBody, validateParams } = require("../../middlewares/zod"); const { itemsZod } = require("./docs/schemas/itemsSchema"); const itemsHandlers = require("../services/items"); @@ -19,7 +19,8 @@ router.use(require("../middlewares/timestampValidator")); router.post( "/purchase/:itemId", - validateParams(itemsZod.purchaseItemHandler), + validateParams(itemsZod.purchaseItemHandlerParams), + validateBody(itemsZod.purchaseItemHandlerBody), itemsHandlers.purchaseItemHandler ); diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index d0288da5..28bf24da 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -53,7 +53,7 @@ const getItemLeaderboardHandler = async (req, res) => { { $group: { _id: "$userId", - amount: { $sum: 1 }, + amount: { $sum: "$itemAmount" }, }, }, { @@ -147,178 +147,205 @@ const updateEventStatus = async ( } ); -const hideItemStock = (item) => { - item.stock = item.stock > 0 ? 1 : 0; - return item; -}; - -const getRandomItem = async (req, depth) => { - if (depth >= 10) { - logger.error(`User ${req.userOid} failed to open random box`); - return null; - } - - const items = await itemModel - .find({ - isRandomItem: true, - stock: { $gt: 0 }, - isDisabled: false, - }) - .lean(); - const randomItems = items - .map((item) => Array(item.randomWeight).fill(item)) - .reduce((a, b) => a.concat(b), []); - const dumpRandomItems = randomItems - .map((item) => item._id.toString()) - .join(","); - - logger.info( - `User ${req.userOid}'s ${ - depth + 1 - }th random box probability is: [${dumpRandomItems}]` - ); - - if (randomItems.length === 0) return null; - - const randomItem = - randomItems[Math.floor(Math.random() * randomItems.length)]; - try { - // 1단계: 재고를 차감합니다. - const newRandomItem = await itemModel - .findOneAndUpdate( - { _id: randomItem._id, stock: { $gt: 0 } }, - { - $inc: { - stock: -1, - }, - }, - { - new: true, - fields: { - itemType: 0, - isRandomItem: 0, - randomWeight: 0, - }, - } - ) - .lean(); - if (!newRandomItem) { - throw new Error(`Item ${randomItem._id.toString()} was already sold out`); - } - - // 2단계: 유저 정보를 업데이트합니다. - await updateEventStatus(req.userOid, { - ticket1Delta: randomItem.itemType === 1 ? 1 : 0, - ticket2Delta: randomItem.itemType === 2 ? 1 : 0, - }); - - // 3단계: Transaction을 추가합니다. - const transaction = new transactionModel({ - type: "use", - amount: 0, - userId: req.userOid, - itemId: randomItem._id, - comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`, - }); - await transaction.save(); - - return newRandomItem; - } catch (err) { - logger.error(err); - logger.warn( - `User ${req.userOid}'s ${depth + 1}th random box failed due to exception` - ); - - return await getRandomItem(req, depth + 1); - } -}; +// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다. +// +// const getRandomItem = async (req, depth) => { +// if (depth >= 10) { +// logger.error(`User ${req.userOid} failed to open random box`); +// return null; +// } + +// const items = await itemModel +// .find({ +// isRandomItem: true, +// stock: { $gt: 0 }, +// isDisabled: false, +// }) +// .lean(); +// const randomItems = items +// .map((item) => Array(item.randomWeight).fill(item)) +// .reduce((a, b) => a.concat(b), []); +// const dumpRandomItems = randomItems +// .map((item) => item._id.toString()) +// .join(","); + +// logger.info( +// `User ${req.userOid}'s ${ +// depth + 1 +// }th random box probability is: [${dumpRandomItems}]` +// ); + +// if (randomItems.length === 0) return null; + +// const randomItem = +// randomItems[Math.floor(Math.random() * randomItems.length)]; +// try { +// // 1단계: 재고를 차감합니다. +// const newRandomItem = await itemModel +// .findOneAndUpdate( +// { _id: randomItem._id, stock: { $gt: 0 } }, +// { +// $inc: { +// stock: -1, +// }, +// }, +// { +// new: true, +// fields: { +// itemType: 0, +// isRandomItem: 0, +// randomWeight: 0, +// }, +// } +// ) +// .lean(); +// if (!newRandomItem) { +// throw new Error(`Item ${randomItem._id.toString()} was already sold out`); +// } + +// // 2단계: 유저 정보를 업데이트합니다. +// await updateEventStatus(req.userOid, { +// ticket1Delta: randomItem.itemType === 1 ? 1 : 0, +// ticket2Delta: randomItem.itemType === 2 ? 1 : 0, +// }); + +// // 3단계: Transaction을 추가합니다. +// const transaction = new transactionModel({ +// type: "use", +// amount: 0, +// userId: req.userOid, +// itemId: randomItem._id, +// comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`, +// }); +// await transaction.save(); + +// return newRandomItem; +// } catch (err) { +// logger.error(err); +// logger.warn( +// `User ${req.userOid}'s ${depth + 1}th random box failed due to exception` +// ); + +// return await getRandomItem(req, depth + 1); +// } +// }; const purchaseItemHandler = async (req, res) => { try { const { itemId } = req.params; - const item = await itemModel.findOne({ _id: itemId }).lean(); + const item = await itemModel.findById(itemId).lean(); if (!item) - return res.status(400).json({ error: "Items/Purchase : invalid Item" }); + return res.status(400).json({ error: "Items/purchase : invalid Item" }); - // 구매 가능 조건: 크레딧이 충분하며, 재고가 남아있으며, 판매 중인 아이템이어야 합니다. + const { amount } = req.body; + const totalPrice = item.price * amount; + + // 구매 가능 조건: 재화가 충분하며, 재고가 남아있으며, 판매 중인 상품이어야 합니다. if (item.isDisabled) - return res.status(400).json({ error: "Items/Purchase : disabled item" }); - if (req.eventStatus.creditAmount < item.price) + return res.status(400).json({ error: "Items/purchase : disabled item" }); + if (req.eventStatus.creditAmount < totalPrice) return res .status(400) - .json({ error: "Items/Purchase : not enough credit" }); - if (item.stock <= 0) + .json({ error: "Items/purchase : not enough credit" }); + if (item.stock < amount) return res .status(400) - .json({ error: "Items/Purchase : item out of stock" }); + .json({ error: "Items/purchase : item out of stock" }); // 1단계: 재고를 차감합니다. const { modifiedCount } = await itemModel.updateOne( - { _id: item._id, stock: { $gt: 0 } }, - { - $inc: { - stock: -1, - }, - } + { _id: item._id, stock: { $gte: amount } }, + { $inc: { stock: -amount } } ); if (modifiedCount === 0) return res .status(400) - .json({ error: "Items/Purchase : item out of stock" }); - - // 2단계: 유저 정보를 업데이트합니다. - await updateEventStatus(req.userOid, { - creditDelta: -item.price, - ticket1Delta: item.itemType === 1 ? 1 : 0, - ticket2Delta: item.itemType === 2 ? 1 : 0, - }); - - // 3단계: Transaction을 추가합니다. - const transaction = new transactionModel({ - type: "use", - amount: item.price, - userId: req.userOid, - itemId: item._id, - comment: `${eventConfig?.credit.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, - }); - await transaction.save(); - - // 4단계: 랜덤박스인 경우 아이템을 추첨합니다. - if (item.itemType !== 3) return res.json({ result: true }); - - const randomItem = await getRandomItem(req, 0); - if (!randomItem) { - // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다. - // TODO: Transactions 도입 후 이 코드는 삭제합니다. - logger.info(`User ${req.userOid}'s status will be restored`); + .json({ error: "Items/purchase : item out of stock" }); - await transactionModel.deleteOne({ _id: transaction._id }); + if (item.itemType !== 3) { + // 랜덤박스가 아닌 상품을 구입한 경우 + // 2단계: 유저 정보를 업데이트합니다. await updateEventStatus(req.userOid, { - creditDelta: item.price, + creditDelta: -totalPrice, + ticket1Delta: item.itemType === 1 ? amount : 0, + ticket2Delta: item.itemType === 2 ? amount : 0, }); - await itemModel.updateOne( - { _id: item._id }, - { - $inc: { - stock: 1, - }, - } - ); - logger.info(`User ${req.userOid}'s status was successfully restored`); + // 3단계: 출금 내역을 추가합니다. + const transaction = new transactionModel({ + type: "use", + amount: totalPrice, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 사용해 "${item.name}" ${amount}개를 획득했습니다.`, + }); + await transaction.save(); + + return res.json({ result: true }); + } else { + // 랜덤박스를 구입한 경우 + // 2단계: 대박(40%)인지 쪽박(60%)인지 결정합니다. + const isJackpot = Math.random() < 0.4; + const creditDelta = isJackpot ? totalPrice : -totalPrice; + + // 3단계: 유저 정보를 업데이트합니다. + await updateEventStatus(req.userOid, { creditDelta }); + + // 4단계: 입출금 내역을 추가합니다. + if (isJackpot) { + const transaction = new transactionModel({ + type: "get", + amount: creditDelta, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용해 대박을 터뜨렸습니다.`, + }); + await transaction.save(); + } else { + const transaction = new transactionModel({ + type: "use", + amount: creditDelta, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용했지만 쪽박을 맞았습니다.`, + }); + await transaction.save(); + } - return res - .status(500) - .json({ error: "Items/Purchase : random box error" }); + return res.json({ result: true, isJackpot }); } - res.json({ - result: true, - reward: hideItemStock(randomItem), - }); + // const randomItem = await getRandomItem(req, 0); + // if (!randomItem) { + // // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다. + // // TODO: Transactions 도입 후 이 코드는 삭제합니다. + // logger.info(`User ${req.userOid}'s status will be restored`); + + // await transactionModel.deleteOne({ _id: transaction._id }); + // await updateEventStatus(req.userOid, { + // creditDelta: item.price, + // }); + // await itemModel.updateOne( + // { _id: item._id }, + // { + // $inc: { + // stock: 1, + // }, + // } + // ); + + // logger.info(`User ${req.userOid}'s status was successfully restored`); + + // return res + // .status(500) + // .json({ error: "Items/purchase : random box error" }); + // } } catch (err) { logger.error(err); - res.status(500).json({ error: "Items/Purchase : internal server error" }); + res.status(500).json({ error: "Items/purchase : internal server error" }); } }; diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js index aee84098..fe976e28 100644 --- a/src/lottery/services/transactions.js +++ b/src/lottery/services/transactions.js @@ -21,12 +21,14 @@ const getUserTransactionsHandler = async (req, res) => { ) .populate(transactionPopulateOption) .lean(); - if (transactions) - res.json({ - transactions: transactions.map(formatTransaction), - }); - else - res.status(500).json({ error: "Transactions/ : internal server error" }); + if (!transactions) + return res + .status(500) + .json({ error: "Transactions/ : internal server error" }); + + res.json({ + transactions: transactions.map(formatTransaction), + }); } catch (err) { logger.error(err); res.status(500).json({ error: "Transactions/ : internal server error" }); From 4fe74d0678e905f089c7364d0c15b0e092e4a00a Mon Sep 17 00:00:00 2001 From: static Date: Sat, 31 Aug 2024 19:25:22 +0900 Subject: [PATCH 089/151] Docs: lottery module --- src/lottery/routes/docs/invites.js | 16 +- src/lottery/routes/docs/items.js | 189 ++++++++++++++---- src/lottery/routes/docs/publicNotice.js | 142 ++++++------- src/lottery/routes/docs/quests.js | 16 +- .../routes/docs/schemas/itemsSchema.js | 99 --------- src/lottery/routes/docs/swaggerDocs.js | 24 +-- 6 files changed, 253 insertions(+), 233 deletions(-) diff --git a/src/lottery/routes/docs/invites.js b/src/lottery/routes/docs/invites.js index 05b0db66..cfe37214 100644 --- a/src/lottery/routes/docs/invites.js +++ b/src/lottery/routes/docs/invites.js @@ -7,15 +7,15 @@ invitesDocs[`${apiPrefix}/search/{inviter}`] = { tags: [`${apiPrefix}`], summary: "초대한 유저의 정보 반환", description: "초대한 유저의 정보를 가져옵니다.", - requestBody: { - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/searchInviterHandler", - }, - }, + parameters: [ + { + in: "path", + name: "inviter", + required: true, + description: "초대한 유저의 eventStatus ObjectId", + example: "INVITER ID", }, - }, + ], responses: { 200: { content: { diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index b08aeaf7..fb2d009d 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -2,15 +2,13 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/items`; const itemsDocs = {}; -itemsDocs[`${apiPrefix}/list`] = { +itemsDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], - summary: "상점에서 판매하는 모든 상품의 목록 반환", - description: - "상점에서 판매하는 모든 상품의 목록을 가져옵니다. 매진된 상품도 가져옵니다.", + summary: "상점에서 판매하는 상품의 목록 반환", + description: "상점에서 판매하는 상품의 목록을 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -19,9 +17,61 @@ itemsDocs[`${apiPrefix}/list`] = { properties: { items: { type: "array", - description: "Item의 배열", + description: "상품의 배열", items: { - $ref: "#/components/schemas/item", + type: "object", + required: [ + "_id", + "name", + "description", + "imageUrl", + "price", + "isDisabled", + "itemType", + ], + properties: { + _id: { + type: "string", + description: "상품의 ObjectId", + example: "ITEM ID", + }, + name: { + type: "string", + description: "상품의 이름", + example: "진짜 송편", + }, + description: { + type: "string", + description: "상품의 설명", + example: "먹을 수 있는 송편입니다.", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "THUMBNAIL URL", + }, + instagramStoryStickerImageUrl: { + type: "string", + description: "인스타그램 스토리 스티커 이미지 URL", + example: "STICKER URL", + }, + price: { + type: "number", + description: "상품의 가격. 0 이상의 정수입니다.", + example: 400, + }, + isDisabled: { + type: "boolean", + description: "상품의 판매 중지 여부", + example: false, + }, + itemType: { + type: "number", + description: + "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", + example: 0, + }, + }, }, }, }, @@ -32,24 +82,111 @@ itemsDocs[`${apiPrefix}/list`] = { }, }, }; -itemsDocs[`${apiPrefix}/purchase/:itemId`] = { +itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { + get: { + tags: [`${apiPrefix}`], + summary: "상품 리더보드 반환", + description: "상품 리더보드를 가져옵니다. 일반 상품만 리더보드를 갖습니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "리더보드를 조회할 상품의 ObjectId", + example: "ITEM ID", + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + required: ["leaderboard"], + properties: { + leaderboard: { + type: "array", + description: "상품 리더보드. 상위 20등까지만 반환됩니다.", + items: { + type: "object", + required: [ + "nickname", + "profileImageUrl", + "amount", + "probability", + ], + properties: { + nickname: { + type: "string", + description: "유저의 닉네임", + example: "static", + }, + profileImageUrl: { + type: "string", + description: "유저의 프로필 이미지 URL", + example: "PROFILE URL", + }, + amount: { + type: "number", + description: "유저가 상품을 구입한 횟수", + example: 3, + }, + probability: { + type: "number", + description: "유저가 상품에 당첨될 확률", + example: 0.1, + }, + }, + }, + }, + rank: { + type: "number", + description: "현재 유저의 리더보드 순위. 1부터 시작합니다.", + example: 1, + }, + amount: { + type: "number", + description: "현재 유저가 상품을 구입한 횟수", + example: 3, + }, + probability: { + type: "number", + description: "현재 유저가 상품에 당첨될 확률", + example: 0.1, + }, + }, + }, + }, + }, + }, + }, + }, +}; +itemsDocs[`${apiPrefix}/purchase/{itemId}`] = { post: { tags: [`${apiPrefix}`], - summary: "상품 구매", - description: "상품을 구매합니다.", + summary: "상품 구입", + description: "상품을 구입합니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "리더보드를 조회할 상품의 ObjectId", + example: "ITEM ID", + }, + ], requestBody: { - description: "", content: { "application/json": { schema: { - $ref: "#/components/schemas/purchaseHandler", + $ref: "#/components/schemas/purchaseItemHandlerBody", }, }, }, }, responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -61,27 +198,11 @@ itemsDocs[`${apiPrefix}/purchase/:itemId`] = { description: "성공 여부. 항상 true입니다.", example: true, }, - reward: { - $ref: "#/components/schemas/rewardItem", - }, - }, - }, - }, - }, - }, - 400: { - description: - "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.", - content: { - "application/json": { - schema: { - type: "object", - required: ["error"], - properties: { - error: { - type: "string", - description: "", - example: "checkBanned: banned user", + isJackpot: { + type: "boolean", + description: + "대박 여부. 랜덤박스를 구입한 경우에만 포함됩니다.", + example: true, }, }, }, diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index 23a410b2..bcf2cc78 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -2,7 +2,7 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/publicNotice`; const publicNoticeDocs = {}; -// 다음 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// 다음 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다. // // publicNoticeDocs[`${apiPrefix}/recentTransactions`] = { // get: { @@ -35,75 +35,75 @@ const publicNoticeDocs = {}; // }, // }, // }; -publicNoticeDocs[`${apiPrefix}/leaderboard`] = { - get: { - tags: [`${apiPrefix}`], - summary: "리더보드 반환", - description: - "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.", - responses: { - 200: { - description: "", - content: { - "application/json": { - schema: { - type: "object", - required: ["leaderboard"], - properties: { - leaderboard: { - type: "array", - description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", - items: { - type: "object", - required: [ - "group", - "creditAmount", - "mvpNickname", - "mvpProfileImageUrl", - ], - properties: { - group: { - type: "number", - description: "새터반", - example: 16, - }, - creditAmount: { - type: "number", - description: "새터반에 소속된 유저의 전체 재화 개수", - example: 3000, - }, - mvpNickname: { - type: "string", - description: - "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임", - example: "asdf", - }, - mvpProfileImageUrl: { - type: "string", - description: "MVP의 프로필 이미지 URL", - example: "IMAGE URL", - }, - }, - }, - }, - group: { - type: "number", - description: "유저의 소속 새터반", - example: 16, - }, - rank: { - type: "number", - description: - "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.", - example: 1, - }, - }, - }, - }, - }, - }, - }, - }, -}; +// publicNoticeDocs[`${apiPrefix}/leaderboard`] = { +// get: { +// tags: [`${apiPrefix}`], +// summary: "리더보드 반환", +// description: +// "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.", +// responses: { +// 200: { +// description: "", +// content: { +// "application/json": { +// schema: { +// type: "object", +// required: ["leaderboard"], +// properties: { +// leaderboard: { +// type: "array", +// description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", +// items: { +// type: "object", +// required: [ +// "group", +// "creditAmount", +// "mvpNickname", +// "mvpProfileImageUrl", +// ], +// properties: { +// group: { +// type: "number", +// description: "새터반", +// example: 16, +// }, +// creditAmount: { +// type: "number", +// description: "새터반에 소속된 유저의 전체 재화 개수", +// example: 3000, +// }, +// mvpNickname: { +// type: "string", +// description: +// "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임", +// example: "asdf", +// }, +// mvpProfileImageUrl: { +// type: "string", +// description: "MVP의 프로필 이미지 URL", +// example: "IMAGE URL", +// }, +// }, +// }, +// }, +// group: { +// type: "number", +// description: "유저의 소속 새터반", +// example: 16, +// }, +// rank: { +// type: "number", +// description: +// "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.", +// example: 1, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }; module.exports = publicNoticeDocs; diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js index ea8459f1..42a9c022 100644 --- a/src/lottery/routes/docs/quests.js +++ b/src/lottery/routes/docs/quests.js @@ -7,15 +7,15 @@ questsDocs[`${apiPrefix}/complete/{questId}`] = { tags: [`${apiPrefix}`], summary: "퀘스트 완료 요청", description: "퀘스트의 완료를 요청합니다.", - requestBody: { - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/completeQuestHandler", - }, - }, + parameters: [ + { + in: "path", + name: "questId", + required: true, + description: "완료를 요청할 퀘스트의 ID", + example: "QUEST ID", }, - }, + ], responses: { 200: { content: { diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index fda71fcc..7e570b5a 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -17,102 +17,3 @@ const itemsZod = { const itemsSchema = zodToSchemaObject(itemsZod); module.exports = { itemsZod, itemsSchema }; - -/* Item에 대한 기본적인 프로퍼티를 갖고 있는 스키마입니다. - * TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다. - */ -const itemBase = { - type: "object", - required: [ - "_id", - "name", - "imageUrl", - "price", - "description", - "isDisabled", - "stock", - ], - properties: { - _id: { - type: "string", - description: "Item의 ObjectId", - example: "OBJECT ID", - }, - name: { - type: "string", - description: "상품의 이름", - example: "진짜송편", - }, - imageUrl: { - type: "string", - description: "이미지 썸네일 URL", - example: "THUMBNAIL URL", - }, - instagramStoryStickerImageUrl: { - type: "string", - description: "인스타그램 스토리 스티커 이미지 URL", - example: "STICKER URL", - }, - price: { - type: "number", - description: "상품의 가격. 0 이상입니다.", - example: 400, - }, - description: { - type: "string", - description: "상품의 설명", - example: "맛있는 송편입니다.", - }, - isDisabled: { - type: "boolean", - description: "판매 중지 여부", - example: false, - }, - stock: { - type: "number", - description: "남은 상품 재고. 재고가 있는 경우 1, 없는 경우 0입니다.", - example: 1, - }, - }, -}; - -/** itemBase에 itemType(상품 유형) 프로퍼티가 추가된 스키마입니다. */ -const itemWithType = { - type: itemBase.type, - required: itemBase.required.concat(["itemType"]), - properties: { - ...itemBase.properties, - itemType: { - type: "number", - description: - "상품 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", - example: 0, - }, - }, -}; - -// const itemsSchema = { -// item: itemWithType, -// relatedItem: { -// ...itemWithType, -// description: -// "Transaction과 관련된 아이템의 Object. 아이템과 관련된 Transaction인 경우에만 포함됩니다.", -// }, -// rewardItem: { -// ...itemBase, -// description: "랜덤박스를 구입한 경우에만 포함됩니다.", -// }, -// purchaseHandler: { -// type: "object", -// required: ["itemId"], -// properties: { -// itemId: { -// type: "string", -// pattern: "^[a-fA-F\\d]{24}$", -// }, -// }, -// errorMessage: "validation: bad request", -// }, -// }; - -// module.exports = itemsSchema; diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index 6dcec7f5..0b6702da 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -7,7 +7,7 @@ const transactionsDocs = require("./transactions"); const { globalStateSchema } = require("./schemas/globalStateSchema"); const { invitesSchema } = require("./schemas/invitesSchema"); -const itemsSchema = require("./schemas/itemsSchema"); +const { itemsSchema } = require("./schemas/itemsSchema"); const { questsSchema } = require("./schemas/questsSchema"); const { eventConfig } = require("../../../../loadenv"); @@ -23,30 +23,28 @@ const eventSwaggerDocs = { name: `${apiPrefix}/invites`, description: "이벤트 - 초대 링크 관련 API", }, - // 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다. - // - // { - // name: `${apiPrefix}/items`, - // description: "이벤트 - 아이템 관련 API", - // }, { - name: `${apiPrefix}/publicNotice`, - description: "이벤트 - 아이템 구매, 뽑기, 획득 공지 관련 API", + name: `${apiPrefix}/items`, + description: "이벤트 - 상품 관련 API", }, + // { + // name: `${apiPrefix}/publicNotice`, + // description: "이벤트 - 상품 구매, 뽑기, 획득 공지 관련 API", + // }, { name: `${apiPrefix}/quests`, description: "이벤트 - 퀘스트 관련 API", }, { name: `${apiPrefix}/transactions`, - description: "이벤트 - 입출금 내역 관련 API", + description: "이벤트 - 재화 입출금 내역 관련 API", }, ], paths: { ...globalStateDocs, ...invitesDocs, - //...itemsDocs, - ...publicNoticeDocs, + ...itemsDocs, + // ...publicNoticeDocs, ...questsDocs, ...transactionsDocs, }, @@ -54,7 +52,7 @@ const eventSwaggerDocs = { schemas: { ...globalStateSchema, ...invitesSchema, - //...itemsSchema, + ...itemsSchema, ...questsSchema, }, }, From 6f9a4348d31ffe9fd0e1d32c12b2c8b0436fd30f Mon Sep 17 00:00:00 2001 From: static Date: Sat, 31 Aug 2024 20:48:05 +0900 Subject: [PATCH 090/151] Remove: disable publicNotice router --- src/lottery/index.js | 2 +- src/lottery/routes/publicNotice.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lottery/index.js b/src/lottery/index.js index 30ace7ac..a00e3598 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -31,7 +31,7 @@ lotteryRouter.use("/globalState", require("./routes/globalState")); lotteryRouter.use("/invites", require("./routes/invites")); lotteryRouter.use("/transactions", require("./routes/transactions")); lotteryRouter.use("/items", require("./routes/items")); -lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); +// lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); lotteryRouter.use("/quests", require("./routes/quests")); // [AdminJS] AdminJS에 표시할 Resource 생성 diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js index 4698a193..f6646061 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -3,10 +3,10 @@ const express = require("express"); const router = express.Router(); const publicNoticeHandlers = require("../services/publicNotice"); -router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); - -// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// 아래의 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다. // +// router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); + // router.get( // "/recentTransactions", // publicNoticeHandlers.getRecentPurchaceItemListHandler From d7e742d7fba54338df4ae753c515d59fbca706c5 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 1 Sep 2024 16:47:08 +0900 Subject: [PATCH 091/151] Fix: invalid format of completedQuests element --- src/lottery/modules/quests.js | 2 +- src/lottery/services/items.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 6b4b1266..0d79ac81 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -120,7 +120,7 @@ const completeQuest = async (userId, timestamp, quest) => { }, $push: { completedQuests: { - id: quest.id, + questId: quest.id, completedAt: timestamp, }, }, diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 28bf24da..47770a2e 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -8,6 +8,7 @@ const { isLogin, getLoginInfo } = require("../../modules/auths/login"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); +const contracts = require("../modules/contracts"); const getItemsHandler = async (req, res) => { try { @@ -282,6 +283,12 @@ const purchaseItemHandler = async (req, res) => { }); await transaction.save(); + // 4단계: 퀘스트를 완료 처리합니다. + await contracts.completeItemPurchaseQuest( + req.userOid, + transaction.createdAt + ); + return res.json({ result: true }); } else { // 랜덤박스를 구입한 경우 From 4a1f9af6d84a03ee1ab5c8f4f1b326536d37a7ad Mon Sep 17 00:00:00 2001 From: static Date: Sun, 1 Sep 2024 17:09:26 +0900 Subject: [PATCH 092/151] Fix: invalid eventStatus.inviter value --- src/lottery/services/globalState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 662a6d67..ee01f47e 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -123,7 +123,7 @@ const createUserGlobalStateHandler = async (req, res) => { eventStatus = new eventStatusModel({ userId: req.userOid, creditAmount: eventConfig?.credit.initialAmount ?? 0, - inviter: inviterStatus?._id ?? undefined, + inviter: inviterStatus?.userId ?? undefined, }); await eventStatus.save(); From e7ef57b00fae894a5118fd97143904ff7f7200bb Mon Sep 17 00:00:00 2001 From: static Date: Sun, 1 Sep 2024 17:32:53 +0900 Subject: [PATCH 093/151] Fix: dailyAttendance can be completed infinitely --- src/lottery/modules/contracts.js | 2 +- src/lottery/services/quests.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index c0ab7c31..a415b0af 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -91,7 +91,7 @@ const quests = buildQuests({ "매일 Taxi에 접속하여 출석 체크를 하면 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요. 송편코인을 얻으려면 출석 체크 페이지에서 출석 버튼을 눌러야 해요.", imageUrl: "", reward: 700, - maxCount: 0, + maxCount: 17, isApiRequired: true, }, itemPurchase: { diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js index 3aa586a4..5a0c6ae6 100644 --- a/src/lottery/services/quests.js +++ b/src/lottery/services/quests.js @@ -19,8 +19,8 @@ const completeQuestHandler = async (req, res) => { // 오늘 완료된 dailyAttendance 퀘스트가 있는지 확인합니다. const completedQuest = req.eventStatus.completedQuests.find( - ({ id, completedAt }) => - id === quest.id && + ({ questId, completedAt }) => + questId === quest.id && completedAt >= todayMidnight && completedAt < tomorrowMidnight ); From 05126ee476c4acdc049d1afaefbc8a20d478a273 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 1 Sep 2024 17:48:59 +0900 Subject: [PATCH 094/151] Add: totalAmount, totalUser field into item leaderboard --- src/lottery/modules/stores/mongo.js | 8 +++++++- src/lottery/routes/docs/items.js | 12 +++++++++++- src/lottery/services/items.js | 13 +++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index ffd7a567..09b6c80e 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -106,7 +106,13 @@ const itemSchema = Schema({ required: true, min: 0, validate: integerValidator, - }, + }, // 의미 없는 값, 기존 코드와의 호환성을 위해 남겨둡니다. + realStock: { + type: Number, + required: true, + min: 1, + validate: integerValidator, + }, // 상품의 실제 재고 itemType: { type: Number, enum: [0, 1, 2, 3], diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index fb2d009d..06a6c118 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -102,7 +102,7 @@ itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { "application/json": { schema: { type: "object", - required: ["leaderboard"], + required: ["leaderboard", "totalAmount", "totalUser"], properties: { leaderboard: { type: "array", @@ -139,6 +139,16 @@ itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { }, }, }, + totalAmount: { + type: "number", + description: "상품의 총 판매량", + example: 100, + }, + totalUser: { + type: "number", + description: "상품을 구입한 유저의 수", + example: 50, + }, rank: { type: "number", description: "현재 유저의 리더보드 순위. 1부터 시작합니다.", diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 47770a2e..9196ad53 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -26,10 +26,13 @@ const getItemsHandler = async (req, res) => { }; // 유도 과정은 services/publicNotice.js 파일에 정의된 calculateProbabilityV2 함수의 주석 참조 -const calculateWinProbability = (stock, users, amount, totalAmount) => { - if (users.length <= stock) return 1; +const calculateWinProbability = (realStock, users, amount, totalAmount) => { + if (users.length <= realStock) return 1; - const base = Math.pow(1 - stock / users.length, users.length / totalAmount); + const base = Math.pow( + 1 - realStock / users.length, + users.length / totalAmount + ); return 1 - Math.pow(base, amount); }; @@ -93,7 +96,7 @@ const getItemLeaderboardHandler = async (req, res) => { userId: user._id, amount: user.amount, probability: calculateWinProbability( - item.stock, + item.realStock, users, user.amount, totalAmount @@ -121,6 +124,8 @@ const getItemLeaderboardHandler = async (req, res) => { return res.json({ leaderboard, + totalAmount, + totalUser: users.length, amount: user?.amount, probability: user?.probability, rank: user?.rank, From 1f52f5c5f8fd008bc8013f6f7fc029056df31c86 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Mon, 2 Sep 2024 16:21:17 +0900 Subject: [PATCH 095/151] Add: ban user by sso id --- src/modules/ban.js | 4 ++-- src/modules/stores/mongo.js | 17 ++++------------- src/routes/docs/users.js | 8 ++++---- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/modules/ban.js b/src/modules/ban.js index 3190b533..290e8f86 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -3,9 +3,9 @@ const { banModel } = require("./stores/mongo"); const getMaxValidServiceBanRecord = async (req) => { try { - // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userOid랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 + // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 const bans = await banModel.find({ - userId: req.userOid, + userSid: req.session.loginInfo.sid, expireAt: { $gte: req.timestamp, }, diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 6469d0d9..f236b829 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -28,20 +28,11 @@ const userSchema = Schema({ const banSchema = Schema({ // 정지 시킬 사용자를 기제함. - userId: { type: mongoose.Types.ObjectId, ref: "User", required: true }, + userSid: { type: String, required: true }, // 정지 사유 - reason: { - type: String, - required: true, - }, - bannedAt: { - type: Date, // 정지 당한 시각 - required: true, - }, - expireAt: { - type: Date, // 정지 만료 시각 - required: true, - }, + reason: { type: String, required: true }, + bannedAt: { type: Date, required: true }, // 정지 당한 시각 + expireAt: { type: Date, required: true }, // 정지 만료 시각 // 정지를 당한 서비스를 기제함 serviceName: { type: String, diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index a42c4f4c..ec4532a5 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -346,8 +346,8 @@ usersDocs[`${apiPrefix}/isBanned`] = { properties: { userId: { type: "string", - description: "사용자의 ObjectId", - pattern: objectId.source, + description: "사용자의 SSO ID", + pattern: "monday-sid", }, reason: { type: "string", @@ -409,8 +409,8 @@ usersDocs[`${apiPrefix}/getBanRecord`] = { properties: { userId: { type: "string", - description: "사용자의 ObjectId", - pattern: objectId.source, + description: "사용자의 SSO ID", + example: "monday-sid", }, reason: { type: "string", From f6c49ae70e54bff9b963b7c96faef6c352cdb11c Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Mon, 2 Sep 2024 16:34:54 +0900 Subject: [PATCH 096/151] Add: pull origin dev --- loadenv.js | 13 +- src/lottery/index.js | 4 +- src/lottery/modules/contracts.js | 150 +++--- src/lottery/modules/populates/transactions.js | 5 +- src/lottery/modules/quests.js | 10 +- src/lottery/modules/stores/mongo.js | 48 +- src/lottery/routes/docs/globalState.js | 84 ++-- .../routes/docs/{invite.js => invites.js} | 41 +- src/lottery/routes/docs/items.js | 199 ++++++-- src/lottery/routes/docs/publicNotice.js | 142 +++--- src/lottery/routes/docs/quests.js | 39 +- .../routes/docs/schemas/globalStateSchema.js | 1 - .../{inviteSchema.js => invitesSchema.js} | 6 +- .../routes/docs/schemas/itemsSchema.js | 109 +---- .../routes/docs/schemas/questsSchema.js | 4 +- src/lottery/routes/docs/swaggerDocs.js | 34 +- src/lottery/routes/docs/transactions.js | 35 +- src/lottery/routes/globalState.js | 3 +- src/lottery/routes/invite.js | 20 - src/lottery/routes/invites.js | 21 + src/lottery/routes/items.js | 34 +- src/lottery/routes/publicNotice.js | 6 +- src/lottery/routes/quests.js | 9 +- src/lottery/routes/transactions.js | 2 +- src/lottery/services/globalState.js | 112 ++--- src/lottery/services/invite.js | 66 --- src/lottery/services/invites.js | 73 +++ src/lottery/services/items.js | 458 ++++++++++++------ src/lottery/services/quests.js | 26 +- src/lottery/services/transactions.js | 31 +- src/modules/fare.js | 35 +- src/sampleGenerator/sampleData.json | 4 +- src/services/fare.js | 83 +--- src/services/rooms.js | 14 +- 34 files changed, 1036 insertions(+), 885 deletions(-) rename src/lottery/routes/docs/{invite.js => invites.js} (60%) rename src/lottery/routes/docs/schemas/{inviteSchema.js => invitesSchema.js} (67%) delete mode 100644 src/lottery/routes/invite.js create mode 100644 src/lottery/routes/invites.js delete mode 100644 src/lottery/services/invite.js create mode 100644 src/lottery/services/invites.js diff --git a/loadenv.js b/loadenv.js index cbf47d88..4e589543 100644 --- a/loadenv.js +++ b/loadenv.js @@ -43,7 +43,18 @@ module.exports = { slackWebhookUrl: { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, - eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional + eventConfig: (process.env.EVENT_CONFIG && + JSON.parse(process.env.EVENT_CONFIG)) || { + mode: "2024fall", + credit: { + name: "송편코인", + initialAmount: 0, + }, + period: { + startAt: new Date("2024-09-07T00:00:00+09:00"), + endAt: new Date("2024-09-24T00:00:00+09:00"), + }, + }, // optional naverMap: { apiId: process.env.NAVER_MAP_API_ID, // optional apiKey: process.env.NAVER_MAP_API_KEY, //optional diff --git a/src/lottery/index.js b/src/lottery/index.js index d485dfe1..a00e3598 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -28,10 +28,10 @@ lotteryRouter.use(require("../middlewares/originValidator")); // [Router] APIs lotteryRouter.use("/globalState", require("./routes/globalState")); -lotteryRouter.use("/invite", require("./routes/invite")); +lotteryRouter.use("/invites", require("./routes/invites")); lotteryRouter.use("/transactions", require("./routes/transactions")); lotteryRouter.use("/items", require("./routes/items")); -lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); +// lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); lotteryRouter.use("/quests", require("./routes/quests")); // [AdminJS] AdminJS에 표시할 Resource 생성 diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 72a62deb..a415b0af 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -13,19 +13,10 @@ const quests = buildQuests({ firstLogin: { name: "첫 발걸음", description: - "로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.", + "이벤트 참여만 해도 송편코인을 얻을 수 있다고?? 이벤트 참여에 동의하고 송편코인을 받아 보세요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstLogin.png", - reward: 50, - }, - payingAndSending: { - name: "함께하는 택시의 여정", - description: - "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 넙죽코인을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", - imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_payingAndSending.png", - reward: 150, - maxCount: 0, + reward: 200, }, firstRoomCreation: { name: "첫 방 개설", @@ -33,33 +24,33 @@ const quests = buildQuests({ "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해보세요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstRoomCreation.png", - reward: 50, + reward: 500, }, roomSharing: { - name: "너 T야? Taxi", + name: "이 택시팟은 진짜 유명한 택시팟임", description: - "방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", + "방을 공유해 친구들을 택시팟에 초대해 보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_roomSharing.png", - reward: 50, + reward: 500, isApiRequired: true, }, - paying: { - name: "정산해요 택시의 숲", + fareSettlement: { + name: "정산의 신, 신팍스", description: - "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산하기를 요청해보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", + "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산을 요청해 보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 + 버튼을 눌러 찾을 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_paying.png", - reward: 100, + reward: 2000, maxCount: 0, }, - sending: { + farePayment: { name: "송금 완료면 I am 신뢰에요", description: "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_sending.png", - reward: 50, + reward: 2000, maxCount: 0, }, nicknameChanging: { @@ -68,7 +59,7 @@ const quests = buildQuests({ "닉네임을 변경하여 자신을 표현하세요. 마이페이지수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_nicknameChanging.png", - reward: 50, + reward: 500, }, accountChanging: { name: "계좌 등록을 해야 능률이 올라갑니다", @@ -76,7 +67,7 @@ const quests = buildQuests({ "정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_accountChanging.png", - reward: 50, + reward: 500, }, adPushAgreement: { name: "Taxi의 소울메이트", @@ -84,25 +75,30 @@ const quests = buildQuests({ "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 때 알려드릴 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_adPushAgreement.png", - reward: 50, + reward: 500, }, eventSharing: { - name: "너 나랑 ㅌ태태택 (1명)", - description: - "내가 초대한 사람이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", + name: "Taxi를 아십니까", + description: "내가 초대한 사람이 이벤트에 참여하면 송편코인을 드려요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", - reward: 50, + reward: 700, maxCount: 0, }, - eventSharing5: { - name: "너 나랑 ㅌ태태택 (5명)", + dailyAttendance: { + name: "하루 한 번 Taxi!", description: - "내가 초대한 사람이 5명이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", - imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", - reward: 250, - maxCount: 0, + "매일 Taxi에 접속하여 출석 체크를 하면 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요. 송편코인을 얻으려면 출석 체크 페이지에서 출석 버튼을 눌러야 해요.", + imageUrl: "", + reward: 700, + maxCount: 17, + isApiRequired: true, + }, + itemPurchase: { + name: "itemPurchase", + description: "itemPurchase", + imageUrl: "", + reward: 500, }, }); @@ -111,40 +107,12 @@ const quests = buildQuests({ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} - * @usage lottery/globalState/createUserGlobalStateHandler + * @usage lottery/globalState - createUserGlobalStateHandler */ const completeFirstLoginQuest = async (userId, timestamp) => { return await completeQuest(userId, timestamp, quests.firstLogin); }; -/** - * payingAndSending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. - * @param {Object} roomObject - 방의 정보입니다. - * @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. - * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. - * @param {Date} roomObject.time - 출발 시각입니다. - * @returns {Promise} - * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitSettlementHandler, rooms - commitPaymentHandler - */ -const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { - logger.info( - `User ${userId} requested to complete payingAndSendingQuest in Room ${roomObject._id}` - ); - - if (roomObject.part.length < 2) return null; - if ( - !eventPeriod || - roomObject.time >= eventPeriod.endAt || - roomObject.time < eventPeriod.startAt - ) - return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - - return await completeQuest(userId, timestamp, quests.payingAndSending); -}; - /** * firstRoomCreation 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. @@ -158,7 +126,7 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { }; /** - * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * fareSettlement 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -169,9 +137,9 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { * @description 정산 요청이 이루어질 때마다 호출해 주세요. * @usage rooms - commitSettlementHandler */ -const completePayingQuest = async (userId, timestamp, roomObject) => { +const completeFareSettlementQuest = async (userId, timestamp, roomObject) => { logger.info( - `User ${userId} requested to complete payingQuest in Room ${roomObject._id}` + `User ${userId} requested to complete fareSettlementQuest in Room ${roomObject._id}` ); if (roomObject.part.length < 2) return null; @@ -182,11 +150,11 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.paying); + return await completeQuest(userId, timestamp, quests.fareSettlement); }; /** - * sending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * farePayment 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -197,9 +165,9 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { * @description 송금이 이루어질 때마다 호출해 주세요. * @usage rooms - commitPaymentHandler */ -const completeSendingQuest = async (userId, timestamp, roomObject) => { +const completeFarePaymentQuest = async (userId, timestamp, roomObject) => { logger.info( - `User ${userId} requested to complete sendingQuest in Room ${roomObject._id}` + `User ${userId} requested to complete farePaymentQuest in Room ${roomObject._id}` ); if (roomObject.part.length < 2) return null; @@ -210,7 +178,7 @@ const completeSendingQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.sending); + return await completeQuest(userId, timestamp, quests.farePayment); }; /** @@ -241,13 +209,13 @@ const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { }; /** - * adPushAgreementQuest 퀘스트의 완료를 요청합니다. + * adPushAgreement 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다. * @returns {Promise} * @description 알림 옵션을 변경할 때마다 호출해 주세요. - * @usage notifications/editOptionsHandler + * @usage notifications - editOptionsHandler */ const completeAdPushAgreementQuest = async ( userId, @@ -260,38 +228,36 @@ const completeAdPushAgreementQuest = async ( }; /** - * eventSharing, eventSharing5 퀘스트의 완료를 요청합니다. + * eventSharing 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} - * @description 초대 링크를 통해 사용자가 이벤트에 참여할 때마다, 초대한 사용자 및 초대받은 사용자에 대해 각각 호출해 주세요. + * @usage lottery/globalState - createUserGlobalStateHandler */ const completeEventSharingQuest = async (userId, timestamp) => { - const eventSharingResult = await completeQuest( - userId, - timestamp, - quests.eventSharing - ); - if (!eventSharingResult || eventSharingResult.questCount % 5 !== 0) - return [eventSharingResult, null]; + return await completeQuest(userId, timestamp, quests.eventSharing); +}; - const eventSharing5Result = await completeQuest( - userId, - timestamp, - quests.eventSharing5 - ); - return [eventSharingResult, eventSharing5Result]; +/** + * itemPurchase 퀘스트의 완료를 요청합니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @returns {Promise} + * @description 상품을 구입할 때마다 호출해 주세요. + */ +const completeItemPurchaseQuest = async (userId, timestamp) => { + return await completeQuest(userId, timestamp, quests.itemPurchase); }; module.exports = { quests, completeFirstLoginQuest, - completePayingAndSendingQuest, completeFirstRoomCreationQuest, - completePayingQuest, - completeSendingQuest, + completeFareSettlementQuest, + completeFarePaymentQuest, completeNicknameChangingQuest, completeAccountChangingQuest, completeAdPushAgreementQuest, completeEventSharingQuest, + completeItemPurchaseQuest, }; diff --git a/src/lottery/modules/populates/transactions.js b/src/lottery/modules/populates/transactions.js index 6d965258..a09428c5 100644 --- a/src/lottery/modules/populates/transactions.js +++ b/src/lottery/modules/populates/transactions.js @@ -1,8 +1,7 @@ const transactionPopulateOption = [ { - path: "item", - select: - "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType", + path: "itemId", + select: "name imageUrl", }, ]; diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 04c6cd4c..0d79ac81 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -14,6 +14,7 @@ const eventPeriod = eventConfig && { }; const requiredQuestFields = ["name", "description", "imageUrl", "reward"]; + const buildQuests = (quests) => { for (const [id, quest] of Object.entries(quests)) { // quest에 필수 필드가 모두 포함되어 있는지 확인합니다. @@ -61,7 +62,7 @@ const buildQuests = (quests) => { * @param {number} quest.reward.credit - 퀘스트의 완료 보상 중 재화의 양입니다. * @param {number} quest.reward.ticket1 - 퀘스트의 완료 보상 중 일반 티켓의 개수입니다. * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. - * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다. + * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화된 경우에도 실패로 처리됩니다. */ const completeQuest = async (userId, timestamp, quest) => { try { @@ -118,7 +119,10 @@ const completeQuest = async (userId, timestamp, quest) => { ticket1Amount: quest.reward.ticket1, }, $push: { - completedQuests: quest.id, + completedQuests: { + questId: quest.id, + completedAt: timestamp, + }, }, } ); @@ -143,7 +147,7 @@ const completeQuest = async (userId, timestamp, quest) => { amount: 0, userId, questId: quest.id, - item: ticket1._id, + itemId: ticket1._id, comment: `"${quest.name}" 퀘스트를 완료해 "${ticket1.name}" ${quest.reward.ticket1}개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 600c99ec..09b6c80e 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -10,6 +10,17 @@ const integerValidator = { message: "{VALUE} is not an integer value", }; +const completedQuestSchema = Schema({ + questId: { + type: String, + required: true, + }, + completedAt: { + type: Date, + required: true, + }, +}); + const eventStatusSchema = Schema({ userId: { type: Schema.Types.ObjectId, @@ -17,7 +28,7 @@ const eventStatusSchema = Schema({ required: true, }, completedQuests: { - type: [String], + type: [completedQuestSchema], default: [], }, creditAmount: { @@ -42,17 +53,11 @@ const eventStatusSchema = Schema({ type: Boolean, default: false, }, - group: { - type: Number, - required: true, - min: 1, - validate: integerValidator, - }, // 소속된 새터반 inviter: { type: Schema.Types.ObjectId, ref: "User", }, // 이 사용자를 초대한 사용자 - isEnabledInviteUrl: { + isInviteUrlEnabled: { type: Boolean, default: false, }, // 초대 링크 활성화 여부 @@ -101,7 +106,13 @@ const itemSchema = Schema({ required: true, min: 0, validate: integerValidator, - }, + }, // 의미 없는 값, 기존 코드와의 호환성을 위해 남겨둡니다. + realStock: { + type: Number, + required: true, + min: 1, + validate: integerValidator, + }, // 상품의 실제 재고 itemType: { type: Number, enum: [0, 1, 2, 3], @@ -124,13 +135,13 @@ const transactionSchema = Schema({ type: String, enum: ["get", "use"], required: true, - }, + }, // get: 재화 획득, use: 재화 사용 amount: { type: Number, required: true, min: 0, validate: integerValidator, - }, + }, // 재화의 변화량의 절댓값 userId: { type: Schema.Types.ObjectId, ref: "User", @@ -138,22 +149,23 @@ const transactionSchema = Schema({ }, questId: { type: String, - }, - item: { + }, // 완료한 퀘스트의 ID + itemId: { type: Schema.Types.ObjectId, ref: `${modelNamePrefix}Item`, - }, - itemType: { + }, // 획득한 상품의 ID + itemAmount: { type: Number, - enum: [0, 1, 2, 3], - }, + min: 1, + validate: integerValidator, + }, // 획득한 상품의 개수 comment: { type: String, required: true, }, }); transactionSchema.set("timestamps", { - createdAt: "createAt", + createdAt: "createdAt", updatedAt: false, }); diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 4af3493e..1bbf23f4 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -5,24 +5,21 @@ const globalStateDocs = {}; globalStateDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], - summary: "Frontend에서 Global state로 관리하는 정보 반환", + summary: "Frontend에서 Global State로 관리하는 정보 반환", description: - "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 가져옵니다.", + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리하는 정보를 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { type: "object", required: [ "isAgreeOnTermsOfEvent", - "isEligible", + "isBanned", "creditAmount", - "groupCreditAmount", - "completedQuests", - "group", "quests", + "completedQuests", ], properties: { isAgreeOnTermsOfEvent: { @@ -30,44 +27,19 @@ globalStateDocs[`${apiPrefix}/`] = { description: "유저의 이벤트 참여 동의 여부", example: true, }, - isEligible: { - type: "boolean", - description: "유저의 이벤트 참여 가능 여부", - example: true, - }, - creditAmount: { - type: "number", - description: "재화 개수. 0 이상입니다.", - example: 1000, - }, - groupCreditAmount: { - type: "number", - description: "소속 새터반에 소속된 유저의 전체 재화 개수", - example: 35000, - }, - completedQuests: { - type: "array", - description: - "유저가 완료한 퀘스트의 배열. 여러 번 완료할 수 있는 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.", - items: { - type: "string", - description: "Quest의 Id", - example: "QUEST ID", - }, - }, isBanned: { type: "boolean", - description: "해당 유저 제재 대상 여부", + description: "유저의 이벤트 참여 제한 여부", example: false, }, - group: { + creditAmount: { type: "number", - description: "유저의 소속 새터반", - example: 16, + description: "유저의 재화 개수. 0 이상의 정수입니다.", + example: 1000, }, quests: { type: "array", - description: "Quest의 배열", + description: "전체 퀘스트의 배열", items: { type: "object", required: [ @@ -82,7 +54,7 @@ globalStateDocs[`${apiPrefix}/`] = { properties: { id: { type: "string", - description: "Quest의 Id", + description: "퀘스트의 Id", example: "QUEST ID", }, name: { @@ -98,34 +70,54 @@ globalStateDocs[`${apiPrefix}/`] = { }, imageUrl: { type: "string", - description: "이미지 썸네일 URL", + description: "퀘스트의 썸네일 이미지 URL", example: "THUMBNAIL URL", }, reward: { type: "object", - description: "완료 보상", required: ["credit"], properties: { credit: { type: "number", - description: "완료 보상 중 재화의 개수입니다.", + description: "퀘스트의 완료 보상 중 재화의 개수", example: 100, }, }, }, maxCount: { type: "number", - description: "최대 완료 가능 횟수", + description: "퀘스트의 최대 완료 가능 횟수", example: 1, }, isApiRequired: { type: "boolean", - description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청할 수 있는지 여부`, + description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청해야 하는지의 여부`, example: false, }, }, }, }, + completedQuests: { + type: "array", + description: + "유저가 완료한 퀘스트의 배열. 여러 번 완료한 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.", + items: { + type: "object", + required: ["id", "completedAt"], + properties: { + id: { + type: "string", + description: "퀘스트의 Id", + example: "QUEST ID", + }, + completedAt: { + type: "string", + description: "퀘스트의 완료 시각", + example: "2023-01-01 00:00:00", + }, + }, + }, + }, }, }, }, @@ -137,11 +129,10 @@ globalStateDocs[`${apiPrefix}/`] = { globalStateDocs[`${apiPrefix}/create`] = { post: { tags: [`${apiPrefix}`], - summary: "Frontend에서 Global state로 관리하는 정보 생성", + summary: "Frontend에서 Global State로 관리할 정보 생성", description: - "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 생성합니다.", + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리할 정보를 생성합니다.", requestBody: { - description: "", content: { "application/json": { schema: { @@ -152,7 +143,6 @@ globalStateDocs[`${apiPrefix}/create`] = { }, responses: { 200: { - description: "", content: { "application/json": { schema: { diff --git a/src/lottery/routes/docs/invite.js b/src/lottery/routes/docs/invites.js similarity index 60% rename from src/lottery/routes/docs/invite.js rename to src/lottery/routes/docs/invites.js index 3a3972da..cfe37214 100644 --- a/src/lottery/routes/docs/invite.js +++ b/src/lottery/routes/docs/invites.js @@ -1,25 +1,23 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig?.mode}/invite`; +const apiPrefix = `/events/${eventConfig?.mode}/invites`; -const inviteDocs = {}; -inviteDocs[`${apiPrefix}/search/:inviter`] = { +const invitesDocs = {}; +invitesDocs[`${apiPrefix}/search/{inviter}`] = { get: { tags: [`${apiPrefix}`], - summary: "초대자 정보 조회", - description: "초대자의 정보를 조회합니다.", - requestBody: { - description: "", - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/searchInviterHandler", - }, - }, + summary: "초대한 유저의 정보 반환", + description: "초대한 유저의 정보를 가져옵니다.", + parameters: [ + { + in: "path", + name: "inviter", + required: true, + description: "초대한 유저의 eventStatus ObjectId", + example: "INVITER ID", }, - }, + ], responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -28,13 +26,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = { properties: { nickname: { type: "string", - description: "초대자의 닉네임", - example: "asdf", + description: "초대한 유저의 닉네임", + example: "static", }, profileImageUrl: { type: "string", - description: "초대자의 프로필 이미지 URL", - example: "IMAGE URL", + description: "초대한 유저의 프로필 이미지 URL", + example: "PROFILE URL", }, }, }, @@ -44,14 +42,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = { }, }, }; -inviteDocs[`${apiPrefix}/create`] = { +invitesDocs[`${apiPrefix}/create`] = { post: { tags: [`${apiPrefix}`], summary: "초대 링크 생성", description: "초대 링크를 생성합니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -72,4 +69,4 @@ inviteDocs[`${apiPrefix}/create`] = { }, }; -module.exports = inviteDocs; +module.exports = invitesDocs; diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index b08aeaf7..06a6c118 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -2,15 +2,13 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/items`; const itemsDocs = {}; -itemsDocs[`${apiPrefix}/list`] = { +itemsDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], - summary: "상점에서 판매하는 모든 상품의 목록 반환", - description: - "상점에서 판매하는 모든 상품의 목록을 가져옵니다. 매진된 상품도 가져옵니다.", + summary: "상점에서 판매하는 상품의 목록 반환", + description: "상점에서 판매하는 상품의 목록을 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -19,9 +17,61 @@ itemsDocs[`${apiPrefix}/list`] = { properties: { items: { type: "array", - description: "Item의 배열", + description: "상품의 배열", items: { - $ref: "#/components/schemas/item", + type: "object", + required: [ + "_id", + "name", + "description", + "imageUrl", + "price", + "isDisabled", + "itemType", + ], + properties: { + _id: { + type: "string", + description: "상품의 ObjectId", + example: "ITEM ID", + }, + name: { + type: "string", + description: "상품의 이름", + example: "진짜 송편", + }, + description: { + type: "string", + description: "상품의 설명", + example: "먹을 수 있는 송편입니다.", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "THUMBNAIL URL", + }, + instagramStoryStickerImageUrl: { + type: "string", + description: "인스타그램 스토리 스티커 이미지 URL", + example: "STICKER URL", + }, + price: { + type: "number", + description: "상품의 가격. 0 이상의 정수입니다.", + example: 400, + }, + isDisabled: { + type: "boolean", + description: "상품의 판매 중지 여부", + example: false, + }, + itemType: { + type: "number", + description: + "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", + example: 0, + }, + }, }, }, }, @@ -32,24 +82,121 @@ itemsDocs[`${apiPrefix}/list`] = { }, }, }; -itemsDocs[`${apiPrefix}/purchase/:itemId`] = { +itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { + get: { + tags: [`${apiPrefix}`], + summary: "상품 리더보드 반환", + description: "상품 리더보드를 가져옵니다. 일반 상품만 리더보드를 갖습니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "리더보드를 조회할 상품의 ObjectId", + example: "ITEM ID", + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + required: ["leaderboard", "totalAmount", "totalUser"], + properties: { + leaderboard: { + type: "array", + description: "상품 리더보드. 상위 20등까지만 반환됩니다.", + items: { + type: "object", + required: [ + "nickname", + "profileImageUrl", + "amount", + "probability", + ], + properties: { + nickname: { + type: "string", + description: "유저의 닉네임", + example: "static", + }, + profileImageUrl: { + type: "string", + description: "유저의 프로필 이미지 URL", + example: "PROFILE URL", + }, + amount: { + type: "number", + description: "유저가 상품을 구입한 횟수", + example: 3, + }, + probability: { + type: "number", + description: "유저가 상품에 당첨될 확률", + example: 0.1, + }, + }, + }, + }, + totalAmount: { + type: "number", + description: "상품의 총 판매량", + example: 100, + }, + totalUser: { + type: "number", + description: "상품을 구입한 유저의 수", + example: 50, + }, + rank: { + type: "number", + description: "현재 유저의 리더보드 순위. 1부터 시작합니다.", + example: 1, + }, + amount: { + type: "number", + description: "현재 유저가 상품을 구입한 횟수", + example: 3, + }, + probability: { + type: "number", + description: "현재 유저가 상품에 당첨될 확률", + example: 0.1, + }, + }, + }, + }, + }, + }, + }, + }, +}; +itemsDocs[`${apiPrefix}/purchase/{itemId}`] = { post: { tags: [`${apiPrefix}`], - summary: "상품 구매", - description: "상품을 구매합니다.", + summary: "상품 구입", + description: "상품을 구입합니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "리더보드를 조회할 상품의 ObjectId", + example: "ITEM ID", + }, + ], requestBody: { - description: "", content: { "application/json": { schema: { - $ref: "#/components/schemas/purchaseHandler", + $ref: "#/components/schemas/purchaseItemHandlerBody", }, }, }, }, responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -61,27 +208,11 @@ itemsDocs[`${apiPrefix}/purchase/:itemId`] = { description: "성공 여부. 항상 true입니다.", example: true, }, - reward: { - $ref: "#/components/schemas/rewardItem", - }, - }, - }, - }, - }, - }, - 400: { - description: - "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.", - content: { - "application/json": { - schema: { - type: "object", - required: ["error"], - properties: { - error: { - type: "string", - description: "", - example: "checkBanned: banned user", + isJackpot: { + type: "boolean", + description: + "대박 여부. 랜덤박스를 구입한 경우에만 포함됩니다.", + example: true, }, }, }, diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index 23a410b2..bcf2cc78 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -2,7 +2,7 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/publicNotice`; const publicNoticeDocs = {}; -// 다음 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// 다음 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다. // // publicNoticeDocs[`${apiPrefix}/recentTransactions`] = { // get: { @@ -35,75 +35,75 @@ const publicNoticeDocs = {}; // }, // }, // }; -publicNoticeDocs[`${apiPrefix}/leaderboard`] = { - get: { - tags: [`${apiPrefix}`], - summary: "리더보드 반환", - description: - "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.", - responses: { - 200: { - description: "", - content: { - "application/json": { - schema: { - type: "object", - required: ["leaderboard"], - properties: { - leaderboard: { - type: "array", - description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", - items: { - type: "object", - required: [ - "group", - "creditAmount", - "mvpNickname", - "mvpProfileImageUrl", - ], - properties: { - group: { - type: "number", - description: "새터반", - example: 16, - }, - creditAmount: { - type: "number", - description: "새터반에 소속된 유저의 전체 재화 개수", - example: 3000, - }, - mvpNickname: { - type: "string", - description: - "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임", - example: "asdf", - }, - mvpProfileImageUrl: { - type: "string", - description: "MVP의 프로필 이미지 URL", - example: "IMAGE URL", - }, - }, - }, - }, - group: { - type: "number", - description: "유저의 소속 새터반", - example: 16, - }, - rank: { - type: "number", - description: - "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.", - example: 1, - }, - }, - }, - }, - }, - }, - }, - }, -}; +// publicNoticeDocs[`${apiPrefix}/leaderboard`] = { +// get: { +// tags: [`${apiPrefix}`], +// summary: "리더보드 반환", +// description: +// "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.", +// responses: { +// 200: { +// description: "", +// content: { +// "application/json": { +// schema: { +// type: "object", +// required: ["leaderboard"], +// properties: { +// leaderboard: { +// type: "array", +// description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", +// items: { +// type: "object", +// required: [ +// "group", +// "creditAmount", +// "mvpNickname", +// "mvpProfileImageUrl", +// ], +// properties: { +// group: { +// type: "number", +// description: "새터반", +// example: 16, +// }, +// creditAmount: { +// type: "number", +// description: "새터반에 소속된 유저의 전체 재화 개수", +// example: 3000, +// }, +// mvpNickname: { +// type: "string", +// description: +// "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임", +// example: "asdf", +// }, +// mvpProfileImageUrl: { +// type: "string", +// description: "MVP의 프로필 이미지 URL", +// example: "IMAGE URL", +// }, +// }, +// }, +// }, +// group: { +// type: "number", +// description: "유저의 소속 새터반", +// example: 16, +// }, +// rank: { +// type: "number", +// description: +// "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.", +// example: 1, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }; module.exports = publicNoticeDocs; diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js index 14694f3e..42a9c022 100644 --- a/src/lottery/routes/docs/quests.js +++ b/src/lottery/routes/docs/quests.js @@ -2,24 +2,22 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/quests`; const questsDocs = {}; -questsDocs[`${apiPrefix}/complete/:questId`] = { +questsDocs[`${apiPrefix}/complete/{questId}`] = { post: { tags: [`${apiPrefix}`], summary: "퀘스트 완료 요청", description: "퀘스트의 완료를 요청합니다.", - requestBody: { - description: "", - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/completeHandler", - }, - }, + parameters: [ + { + in: "path", + name: "questId", + required: true, + description: "완료를 요청할 퀘스트의 ID", + example: "QUEST ID", }, - }, + ], responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -36,25 +34,6 @@ questsDocs[`${apiPrefix}/complete/:questId`] = { }, }, }, - 400: { - description: - "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.", - content: { - "application/json": { - schema: { - type: "object", - required: ["error"], - properties: { - error: { - type: "string", - description: "", - example: "checkBanned: banned user", - }, - }, - }, - }, - }, - }, }, }, }; diff --git a/src/lottery/routes/docs/schemas/globalStateSchema.js b/src/lottery/routes/docs/schemas/globalStateSchema.js index 0c3c55e6..15055525 100644 --- a/src/lottery/routes/docs/schemas/globalStateSchema.js +++ b/src/lottery/routes/docs/schemas/globalStateSchema.js @@ -6,7 +6,6 @@ const globalStateZod = { createUserGlobalStateHandler: z .object({ phoneNumber: z.string().regex(user.phoneNumber), - group: z.number().gte(1).lte(26), inviter: z.string().regex(objectId), }) .partial({ inviter: true }), diff --git a/src/lottery/routes/docs/schemas/inviteSchema.js b/src/lottery/routes/docs/schemas/invitesSchema.js similarity index 67% rename from src/lottery/routes/docs/schemas/inviteSchema.js rename to src/lottery/routes/docs/schemas/invitesSchema.js index e3016557..dfc33c5c 100644 --- a/src/lottery/routes/docs/schemas/inviteSchema.js +++ b/src/lottery/routes/docs/schemas/invitesSchema.js @@ -2,12 +2,12 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const { objectId } = require("../../../../modules/patterns"); -const inviteZod = { +const invitesZod = { searchInviterHandler: z.object({ inviter: z.string().regex(objectId), }), }; -const inviteSchema = zodToSchemaObject(inviteZod); +const invitesSchema = zodToSchemaObject(invitesZod); -module.exports = { inviteSchema, inviteZod }; +module.exports = { invitesZod, invitesSchema }; diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index 80912cfb..7e570b5a 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -1,98 +1,19 @@ -/* Item에 대한 기본적인 프로퍼티를 갖고 있는 스키마입니다. - * TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다. - */ -const itemBase = { - type: "object", - required: [ - "_id", - "name", - "imageUrl", - "price", - "description", - "isDisabled", - "stock", - ], - properties: { - _id: { - type: "string", - description: "Item의 ObjectId", - example: "OBJECT ID", - }, - name: { - type: "string", - description: "상품의 이름", - example: "진짜송편", - }, - imageUrl: { - type: "string", - description: "이미지 썸네일 URL", - example: "THUMBNAIL URL", - }, - instagramStoryStickerImageUrl: { - type: "string", - description: "인스타그램 스토리 스티커 이미지 URL", - example: "STICKER URL", - }, - price: { - type: "number", - description: "상품의 가격. 0 이상입니다.", - example: 400, - }, - description: { - type: "string", - description: "상품의 설명", - example: "맛있는 송편입니다.", - }, - isDisabled: { - type: "boolean", - description: "판매 중지 여부", - example: false, - }, - stock: { - type: "number", - description: "남은 상품 재고. 재고가 있는 경우 1, 없는 경우 0입니다.", - example: 1, - }, - }, -}; +const { z } = require("zod"); +const { zodToSchemaObject } = require("../../../../routes/docs/utils"); +const { objectId } = require("../../../../modules/patterns"); -/** itemBase에 itemType(상품 유형) 프로퍼티가 추가된 스키마입니다. */ -const itemWithType = { - type: itemBase.type, - required: itemBase.required.concat(["itemType"]), - properties: { - ...itemBase.properties, - itemType: { - type: "number", - description: - "상품 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", - example: 0, - }, - }, +const itemsZod = { + getItemLeaderboardHandler: z.object({ + itemId: z.string().regex(objectId), + }), + purchaseItemHandlerParams: z.object({ + itemId: z.string().regex(objectId), + }), + purchaseItemHandlerBody: z.object({ + amount: z.number().int().positive(), + }), }; -const itemsSchema = { - item: itemWithType, - relatedItem: { - ...itemWithType, - description: - "Transaction과 관련된 아이템의 Object. 아이템과 관련된 Transaction인 경우에만 포함됩니다.", - }, - rewardItem: { - ...itemBase, - description: "랜덤박스를 구입한 경우에만 포함됩니다.", - }, - purchaseHandler: { - type: "object", - required: ["itemId"], - properties: { - itemId: { - type: "string", - pattern: "^[a-fA-F\\d]{24}$", - }, - }, - errorMessage: "validation: bad request", - }, -}; +const itemsSchema = zodToSchemaObject(itemsZod); -module.exports = itemsSchema; +module.exports = { itemsZod, itemsSchema }; diff --git a/src/lottery/routes/docs/schemas/questsSchema.js b/src/lottery/routes/docs/schemas/questsSchema.js index 2efd11cd..8daf560d 100644 --- a/src/lottery/routes/docs/schemas/questsSchema.js +++ b/src/lottery/routes/docs/schemas/questsSchema.js @@ -2,7 +2,9 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const questsZod = { - completeHandler: z.object({ questId: z.enum(["roomSharing"]) }), + completeQuestHandler: z.object({ + questId: z.enum(["roomSharing", "dailyAttendance"]), + }), }; const questsSchema = zodToSchemaObject(questsZod); diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index 38ca8298..0b6702da 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -1,13 +1,13 @@ const globalStateDocs = require("./globalState"); -const inviteDocs = require("./invite"); +const invitesDocs = require("./invites"); const itemsDocs = require("./items"); const publicNoticeDocs = require("./publicNotice"); const questsDocs = require("./quests"); const transactionsDocs = require("./transactions"); const { globalStateSchema } = require("./schemas/globalStateSchema"); -const { inviteSchema } = require("./schemas/inviteSchema"); -const itemsSchema = require("./schemas/itemsSchema"); +const { invitesSchema } = require("./schemas/invitesSchema"); +const { itemsSchema } = require("./schemas/itemsSchema"); const { questsSchema } = require("./schemas/questsSchema"); const { eventConfig } = require("../../../../loadenv"); @@ -20,41 +20,39 @@ const eventSwaggerDocs = { description: "이벤트 - Global State 관련 API", }, { - name: `${apiPrefix}/invite`, + name: `${apiPrefix}/invites`, description: "이벤트 - 초대 링크 관련 API", }, - // 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다. - // - // { - // name: `${apiPrefix}/items`, - // description: "이벤트 - 아이템 관련 API", - // }, { - name: `${apiPrefix}/publicNotice`, - description: "이벤트 - 아이템 구매, 뽑기, 획득 공지 관련 API", + name: `${apiPrefix}/items`, + description: "이벤트 - 상품 관련 API", }, + // { + // name: `${apiPrefix}/publicNotice`, + // description: "이벤트 - 상품 구매, 뽑기, 획득 공지 관련 API", + // }, { name: `${apiPrefix}/quests`, description: "이벤트 - 퀘스트 관련 API", }, { name: `${apiPrefix}/transactions`, - description: "이벤트 - 입출금 내역 관련 API", + description: "이벤트 - 재화 입출금 내역 관련 API", }, ], paths: { ...globalStateDocs, - ...inviteDocs, - //...itemsDocs, - ...publicNoticeDocs, + ...invitesDocs, + ...itemsDocs, + // ...publicNoticeDocs, ...questsDocs, ...transactionsDocs, }, components: { schemas: { ...globalStateSchema, - ...inviteSchema, - //...itemsSchema, + ...invitesSchema, + ...itemsSchema, ...questsSchema, }, }, diff --git a/src/lottery/routes/docs/transactions.js b/src/lottery/routes/docs/transactions.js index fa78238b..a041b949 100644 --- a/src/lottery/routes/docs/transactions.js +++ b/src/lottery/routes/docs/transactions.js @@ -6,10 +6,9 @@ transactionsDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], summary: "재화 입출금 내역 반환", - description: "유저의 재화 입출금 내역을 가져옵니다.", + description: "재화 입출금 내역을 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -18,16 +17,11 @@ transactionsDocs[`${apiPrefix}/`] = { properties: { transactions: { type: "array", - description: "유저의 재화 입출금 기록의 배열", + description: "유저의 재화 입출금 내역의 배열", items: { type: "object", - required: ["_id", "type", "amount", "comment", "createAt"], + required: ["type", "amount", "comment", "createdAt"], properties: { - _id: { - type: "string", - description: "Transaction의 ObjectId", - example: "OBJECT ID", - }, type: { type: "string", description: @@ -41,18 +35,33 @@ transactionsDocs[`${apiPrefix}/`] = { }, questId: { type: "string", - description: - "Transaction과 관련된 퀘스트의 Id. 퀘스트와 관련된 Transaction인 경우에만 포함됩니다.", + description: "입출금 내역과 관련된 퀘스트의 Id", example: "QUEST ID", }, + item: { + type: "object", + required: ["name", "imageUrl"], + properties: { + name: { + type: "string", + description: "상품의 이름", + example: "랜덤 상자", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "IMAGE URL", + }, + }, + }, comment: { type: "string", description: "입출금 내역에 대한 설명", example: "랜덤 상자 구입 - 50개 차감", }, - createAt: { + createdAt: { type: "string", - description: "입출금이 일어난 시각", + description: "입출금 내역이 생성된 시각", example: "2023-01-01 00:00:00", }, }, diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js index 1f2b4327..c4f37d39 100644 --- a/src/lottery/routes/globalState.js +++ b/src/lottery/routes/globalState.js @@ -1,7 +1,8 @@ const express = require("express"); +const router = express.Router(); + const { validateBody } = require("../../middlewares/zod"); const { globalStateZod } = require("./docs/schemas/globalStateSchema"); -const router = express.Router(); const globalStateHandlers = require("../services/globalState"); router.get("/", globalStateHandlers.getUserGlobalStateHandler); diff --git a/src/lottery/routes/invite.js b/src/lottery/routes/invite.js deleted file mode 100644 index eafa09cb..00000000 --- a/src/lottery/routes/invite.js +++ /dev/null @@ -1,20 +0,0 @@ -const express = require("express"); -const { validateParams } = require("../../middlewares/zod"); -const { inviteZod } = require("./docs/schemas/inviteSchema"); -const router = express.Router(); -const inviteHandlers = require("../services/invite"); - -router.get( - "/search/:inviter", - validateParams(inviteZod.searchInviterHandler), - inviteHandlers.searchInviterHandler -); - -// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 -router.use(require("../../middlewares/auth")); -router.use(require("../middlewares/checkBanned")); -router.use(require("../middlewares/timestampValidator")); - -router.post("/create", inviteHandlers.createInviteUrlHandler); - -module.exports = router; diff --git a/src/lottery/routes/invites.js b/src/lottery/routes/invites.js new file mode 100644 index 00000000..65e4271e --- /dev/null +++ b/src/lottery/routes/invites.js @@ -0,0 +1,21 @@ +const express = require("express"); +const router = express.Router(); + +const { validateParams } = require("../../middlewares/zod"); +const { invitesZod } = require("./docs/schemas/invitesSchema"); +const invitesHandlers = require("../services/invites"); + +router.get( + "/search/:inviter", + validateParams(invitesZod.searchInviterHandler), + invitesHandlers.searchInviterHandler +); + +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 +router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/checkBanned")); +router.use(require("../middlewares/timestampValidator")); + +router.post("/create", invitesHandlers.createInviteUrlHandler); + +module.exports = router; diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 5cdf98a8..135617b8 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -1,23 +1,27 @@ const express = require("express"); - const router = express.Router(); -// TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다. + +const { validateBody, validateParams } = require("../../middlewares/zod"); +const { itemsZod } = require("./docs/schemas/itemsSchema"); const itemsHandlers = require("../services/items"); -const itemsSchema = require("./docs/schemas/itemsSchema"); -// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. -// -// router.get("/list", itemsHandlers.listHandler); +router.get("/", itemsHandlers.getItemsHandler); +router.get( + "/leaderboard/:itemId", + validateParams(itemsZod.getItemLeaderboardHandler), + itemsHandlers.getItemLeaderboardHandler +); -// // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 -// router.use(require("../../middlewares/auth")); -// router.use(require("../middlewares/checkBanned")); -// router.use(require("../middlewares/timestampValidator")); +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 +router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/checkBanned")); +router.use(require("../middlewares/timestampValidator")); -// router.post( -// "/purchase/:itemId", -// validateParams(itemsSchema.purchaseHandler), -// itemsHandlers.purchaseHandler -// ); +router.post( + "/purchase/:itemId", + validateParams(itemsZod.purchaseItemHandlerParams), + validateBody(itemsZod.purchaseItemHandlerBody), + itemsHandlers.purchaseItemHandler +); module.exports = router; diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js index 4698a193..f6646061 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -3,10 +3,10 @@ const express = require("express"); const router = express.Router(); const publicNoticeHandlers = require("../services/publicNotice"); -router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); - -// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// 아래의 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다. // +// router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); + // router.get( // "/recentTransactions", // publicNoticeHandlers.getRecentPurchaceItemListHandler diff --git a/src/lottery/routes/quests.js b/src/lottery/routes/quests.js index 4941c8d2..e9845434 100644 --- a/src/lottery/routes/quests.js +++ b/src/lottery/routes/quests.js @@ -1,18 +1,19 @@ const express = require("express"); +const router = express.Router(); + const { validateParams } = require("../../middlewares/zod"); const { questsZod } = require("./docs/schemas/questsSchema"); -const router = express.Router(); const questsHandlers = require("../services/quests"); -// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 router.use(require("../../middlewares/auth")); router.use(require("../middlewares/checkBanned")); router.use(require("../middlewares/timestampValidator")); router.post( "/complete/:questId", - validateParams(questsZod.completeHandler), - questsHandlers.completeHandler + validateParams(questsZod.completeQuestHandler), + questsHandlers.completeQuestHandler ); module.exports = router; diff --git a/src/lottery/routes/transactions.js b/src/lottery/routes/transactions.js index aee05d90..f9e375ca 100644 --- a/src/lottery/routes/transactions.js +++ b/src/lottery/routes/transactions.js @@ -1,6 +1,6 @@ const express = require("express"); - const router = express.Router(); + const transactionsHandlers = require("../services/transactions"); // 아래의 Endpoint 접근 시 로그인 필요 diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 5459f851..ee01f47e 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -8,61 +8,57 @@ const { eventConfig } = require("../../../loadenv"); const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); -// 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. -const checkIsUserEligible = (user) => { - // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. - if (nodeEnv !== "production") return true; +// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다. +// +// // 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. +// const checkIsUserEligible = (user) => { +// // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. +// if (nodeEnv !== "production") return true; - const kaistId = parseInt(user?.subinfo?.kaist || "0"); - return 20240001 <= kaistId && kaistId <= 20241500; -}; +// const kaistId = parseInt(user?.subinfo?.kaist || "0"); +// return 20240001 <= kaistId && kaistId <= 20241500; +// }; const getUserGlobalStateHandler = async (req, res) => { try { const userId = isLogin(req) ? getLoginInfo(req).oid : null; - const user = userId && (await userModel.findOne({ _id: userId }).lean()); - const eventStatus = userId && (await eventStatusModel - .findOne({ userId }, "completedQuests creditAmount isBanned group") + .findOne({ userId }, "completedQuests creditAmount isBanned") .lean()); if (!eventStatus) return res.json({ isAgreeOnTermsOfEvent: false, - isEligible: checkIsUserEligible(user) || !!user?.isAdmin, // 테스트를 위해 관리자인 경우 true로 설정합니다. 하지만 관리자이더라도 이벤트에 참여할 수 없습니다. - completedQuests: [], + isBanned: false, creditAmount: 0, - group: 0, - groupCreditAmount: 0, quests, + completedQuests: [], }); // group이 eventStatus.group과 같은 사용자들의 creditAmount를 합산합니다. - const groupCreditAmount = await eventStatusModel.aggregate([ - { - $match: { - group: eventStatus.group, - }, - }, - { - $group: { - _id: null, - creditAmount: { $sum: "$creditAmount" }, - }, - }, - ]); - const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount; - if (!groupCreditAmountReal && groupCreditAmountReal !== 0) - return res - .status(500) - .json({ error: "GlobalState/ : internal server error" }); + // const groupCreditAmount = await eventStatusModel.aggregate([ + // { + // $match: { + // group: eventStatus.group, + // }, + // }, + // { + // $group: { + // _id: null, + // creditAmount: { $sum: "$creditAmount" }, + // }, + // }, + // ]); + // const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount; + // if (!groupCreditAmountReal && groupCreditAmountReal !== 0) + // return res + // .status(500) + // .json({ error: "GlobalState/ : internal server error" }); return res.json({ - isAgreeOnTermsOfEvent: true, - isEligible: true, ...eventStatus, - groupCreditAmount: groupCreditAmountReal, + isAgreeOnTermsOfEvent: true, quests, }); } catch (err) { @@ -79,7 +75,7 @@ const createUserGlobalStateHandler = async (req, res) => { if (eventStatus) return res .status(400) - .json({ error: "GlobalState/Create : already created" }); + .json({ error: "GlobalState/create : already created" }); /* Request의 inviter 필드가 설정되어 있는데, 1. 해당되는 유저가 이벤트에 참여하지 않았거나, @@ -88,47 +84,53 @@ const createUserGlobalStateHandler = async (req, res) => { 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ const inviterStatus = req.body.inviter && - (await eventStatusModel.findOne({ _id: req.body.inviter }).lean()); + (await eventStatusModel.findById(req.body.inviter).lean()); if ( req.body.inviter && (!inviterStatus || inviterStatus.isBanned || - !inviterStatus.isEnabledInviteUrl) + !inviterStatus.isInviteUrlEnabled) ) return res.status(400).json({ - error: "GlobalState/Create : inviter did not participate in the event", + error: "GlobalState/create : invalid inviter", }); - const user = await userModel.findOne({ _id: req.userOid }); + const user = await userModel.findById(req.userOid); if (!user) return res .status(500) - .json({ error: "GlobalState/Create : internal server error" }); + .json({ error: "GlobalState/create : internal server error" }); // 유저가 이벤트에 참여할 수 있는지 확인합니다. - const isEligible = checkIsUserEligible(user); - if (!isEligible) - return res.status(400).json({ - error: "GlobalState/Create : not eligible to participate in the event", - }); - - // 수집한 전화번호를 User Document에 저장합니다. - // 다른 이벤트 참여 과정에서 문제가 생길 수 있으므로, 이벤트 참여 자격이 있는 경우에만 저장합니다. - user.phoneNumber = req.body.phoneNumber; - await user.save(); + // const isEligible = checkIsUserEligible(user); + // if (!isEligible) + // return res.status(400).json({ + // error: "GlobalState/create : not eligible to participate in the event", + // }); + + // 필요한 경우 유저의 전화번호를 업데이트합니다. + if (user.phoneNumber !== req.body.phoneNumber) { + if (user.phoneNumber) { + logger.info(`Past user phone number: ${user.phoneNumber}`); + logger.info(`Update user phone number: ${req.body.phoneNumber}`); + } + + user.phoneNumber = req.body.phoneNumber; + await user.save(); + } // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, creditAmount: eventConfig?.credit.initialAmount ?? 0, - group: req.body.group, - inviter: req.body.inviter, + inviter: inviterStatus?.userId ?? undefined, }); await eventStatus.save(); + // 퀘스트를 완료 처리합니다. await contracts.completeFirstLoginQuest(req.userOid, req.timestamp); - if (req.body.inviter) { + if (inviterStatus) { await contracts.completeEventSharingQuest(req.userOid, req.timestamp); await contracts.completeEventSharingQuest( inviterStatus.userId, @@ -141,7 +143,7 @@ const createUserGlobalStateHandler = async (req, res) => { logger.error(err); res .status(500) - .json({ error: "GlobalState/Create : internal server error" }); + .json({ error: "GlobalState/create : internal server error" }); } }; diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js deleted file mode 100644 index c7871273..00000000 --- a/src/lottery/services/invite.js +++ /dev/null @@ -1,66 +0,0 @@ -const { eventStatusModel } = require("../modules/stores/mongo"); -const { userModel } = require("../../modules/stores/mongo"); -const logger = require("../../modules/logger"); - -const { eventConfig } = require("../../../loadenv"); - -const searchInviterHandler = async (req, res) => { - try { - const { inviter } = req.params; - const inviterStatus = await eventStatusModel.findOne({ _id: inviter }); - if ( - !inviterStatus || - !inviterStatus.isEnabledInviteUrl || - inviterStatus.isBanned - ) - return res.status(400).json({ error: "Invite/Search : invalid inviter" }); - - const inviterInfo = await userModel.findOne({ _id: inviterStatus.userId }); - if (!inviterInfo) - return res - .status(500) - .json({ error: "Invite/Search : internal server error" }); - - return res.json({ - nickname: inviterInfo.nickname, - profileImageUrl: inviterInfo.profileImageUrl, - }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Invite/Search : internal server error" }); - } -}; - -const createInviteUrlHandler = async (req, res) => { - try { - const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`; - - if (req.eventStatus.isEnabledInviteUrl) return res.json({ inviteUrl }); - - const eventStatus = await eventStatusModel - .findOneAndUpdate( - { - _id: req.eventStatus._id, - isEnabledInviteUrl: false, - }, - { - isEnabledInviteUrl: true, - } - ) - .lean(); - if (!eventStatus) - return res - .status(500) - .json({ error: "Invite/Create : internal server error" }); - - return res.json({ inviteUrl }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Invite/Create : internal server error" }); - } -}; - -module.exports = { - searchInviterHandler, - createInviteUrlHandler, -}; diff --git a/src/lottery/services/invites.js b/src/lottery/services/invites.js new file mode 100644 index 00000000..6479bce8 --- /dev/null +++ b/src/lottery/services/invites.js @@ -0,0 +1,73 @@ +const { eventStatusModel } = require("../modules/stores/mongo"); +const { userModel } = require("../../modules/stores/mongo"); +const logger = require("../../modules/logger"); + +const { eventConfig } = require("../../../loadenv"); + +const searchInviterHandler = async (req, res) => { + try { + /* 1. 해당되는 유저가 이벤트에 참여하지 않았거나, + 2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나, + 3. 해당되는 유저의 초대 링크가 활성화되지 않았으면, + 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ + const inviterStatus = await eventStatusModel + .findById(req.params.inviter) + .lean(); + if ( + !inviterStatus || + inviterStatus.isBanned || + !inviterStatus.isInviteUrlEnabled + ) + return res + .status(400) + .json({ error: "Invites/search : invalid inviter" }); + + // 해당되는 유저의 닉네임과 프로필 이미지를 가져옵니다. + const inviter = await userModel + .findById(inviterStatus.userId, "nickname profileImageUrl") + .lean(); + if (!inviter) + return res + .status(500) + .json({ error: "Invites/search : internal server error" }); + + return res.json(inviter); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invites/search : internal server error" }); + } +}; + +const createInviteUrlHandler = async (req, res) => { + try { + const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`; + + // 이미 초대 링크가 활성화된 경우 링크를 즉시 반환합니다. + if (req.eventStatus.isInviteUrlEnabled) return res.json({ inviteUrl }); + + // 초대 링크를 활성화합니다. + const { modifiedCount } = await eventStatusModel.updateOne( + { + _id: req.eventStatus._id, + isInviteUrlEnabled: false, + }, + { + isInviteUrlEnabled: true, + } + ); + if (modifiedCount !== 1) + return res + .status(500) + .json({ error: "Invites/create : internal server error" }); + + return res.json({ inviteUrl }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invites/create : internal server error" }); + } +}; + +module.exports = { + searchInviterHandler, + createInviteUrlHandler, +}; diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 9189bae4..9196ad53 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -3,9 +3,140 @@ const { itemModel, transactionModel, } = require("../modules/stores/mongo"); +const { userModel } = require("../../modules/stores/mongo"); +const { isLogin, getLoginInfo } = require("../../modules/auths/login"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); +const contracts = require("../modules/contracts"); + +const getItemsHandler = async (req, res) => { + try { + const items = await itemModel + .find( + {}, + "_id name description imageUrl instagramStoryStickerImageUrl price isDisabled itemType" + ) + .lean(); + res.json({ items }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Items/ : internal server error" }); + } +}; + +// 유도 과정은 services/publicNotice.js 파일에 정의된 calculateProbabilityV2 함수의 주석 참조 +const calculateWinProbability = (realStock, users, amount, totalAmount) => { + if (users.length <= realStock) return 1; + + const base = Math.pow( + 1 - realStock / users.length, + users.length / totalAmount + ); + return 1 - Math.pow(base, amount); +}; + +const getItemLeaderboardHandler = async (req, res) => { + try { + // 상품 정보를 가져옵니다. + const { itemId } = req.params; + const item = await itemModel.findOne({ _id: itemId, itemType: 0 }).lean(); + if (!item) + return res + .status(400) + .json({ error: "Items/leaderboard : invalid item" }); + + // 해당 상품을 구매한 유저들의 목록을 가져옵니다. + const users = await transactionModel.aggregate([ + { + $match: { + type: "use", + itemId: item._id, + }, + }, + { + $group: { + _id: "$userId", + amount: { $sum: "$itemAmount" }, + }, + }, + { + $lookup: { + from: eventStatusModel.collection.name, + localField: "_id", + foreignField: "userId", + as: "eventStatus", + }, + }, + { + $match: { + "eventStatus.0.isBanned": false, + }, + }, + { + $sort: { amount: -1 }, + }, + ]); + + // 리더보드 생성을 위해 필요한 정보를 계산합니다. + const totalAmount = users.reduce((acc, user) => acc + user.amount, 0); + const rankMap = new Map( + users + .map((user) => user.amount) + .reduce((acc, amount, index) => { + if (acc.length === 0 || acc[acc.length - 1][0] !== amount) { + acc.push([amount, index + 1]); + } + return acc; + }, []) + ); + + // 리더보드를 생성합니다. + const leaderboardBase = users.map((user) => ({ + userId: user._id, + amount: user.amount, + probability: calculateWinProbability( + item.realStock, + users, + user.amount, + totalAmount + ), + rank: rankMap.get(user.amount), + })); + const leaderboard = await Promise.all( + leaderboardBase + .filter((user) => user.rank <= 20) + .map(async (user) => { + const userInfo = await userModel.findById(user.userId).lean(); + return { + nickname: userInfo.nickname, + profileImageUrl: userInfo.profileImageUrl, + amount: user.amount, + probability: user.probability, + }; + }) + ); + + const userId = isLogin(req) ? getLoginInfo(req).oid : null; + const user = leaderboardBase.find( + (user) => user.userId.toString() === userId + ); + + return res.json({ + leaderboard, + totalAmount, + totalUser: users.length, + amount: user?.amount, + probability: user?.probability, + rank: user?.rank, + }); + } catch (err) { + logger.error(err); + res + .status(500) + .json({ error: "Items/leaderboard : internal server error" }); + } +}; const updateEventStatus = async ( userId, @@ -22,199 +153,216 @@ const updateEventStatus = async ( } ); -const hideItemStock = (item) => { - item.stock = item.stock > 0 ? 1 : 0; - return item; -}; +// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다. +// +// const getRandomItem = async (req, depth) => { +// if (depth >= 10) { +// logger.error(`User ${req.userOid} failed to open random box`); +// return null; +// } -const getRandomItem = async (req, depth) => { - if (depth >= 10) { - logger.error(`User ${req.userOid} failed to open random box`); - return null; - } +// const items = await itemModel +// .find({ +// isRandomItem: true, +// stock: { $gt: 0 }, +// isDisabled: false, +// }) +// .lean(); +// const randomItems = items +// .map((item) => Array(item.randomWeight).fill(item)) +// .reduce((a, b) => a.concat(b), []); +// const dumpRandomItems = randomItems +// .map((item) => item._id.toString()) +// .join(","); - const items = await itemModel - .find({ - isRandomItem: true, - stock: { $gt: 0 }, - isDisabled: false, - }) - .lean(); - const randomItems = items - .map((item) => Array(item.randomWeight).fill(item)) - .reduce((a, b) => a.concat(b), []); - const dumpRandomItems = randomItems - .map((item) => item._id.toString()) - .join(","); - - logger.info( - `User ${req.userOid}'s ${ - depth + 1 - }th random box probability is: [${dumpRandomItems}]` - ); +// logger.info( +// `User ${req.userOid}'s ${ +// depth + 1 +// }th random box probability is: [${dumpRandomItems}]` +// ); - if (randomItems.length === 0) return null; +// if (randomItems.length === 0) return null; - const randomItem = - randomItems[Math.floor(Math.random() * randomItems.length)]; - try { - // 1단계: 재고를 차감합니다. - const newRandomItem = await itemModel - .findOneAndUpdate( - { _id: randomItem._id, stock: { $gt: 0 } }, - { - $inc: { - stock: -1, - }, - }, - { - new: true, - fields: { - itemType: 0, - isRandomItem: 0, - randomWeight: 0, - }, - } - ) - .lean(); - if (!newRandomItem) { - throw new Error(`Item ${randomItem._id.toString()} was already sold out`); - } +// const randomItem = +// randomItems[Math.floor(Math.random() * randomItems.length)]; +// try { +// // 1단계: 재고를 차감합니다. +// const newRandomItem = await itemModel +// .findOneAndUpdate( +// { _id: randomItem._id, stock: { $gt: 0 } }, +// { +// $inc: { +// stock: -1, +// }, +// }, +// { +// new: true, +// fields: { +// itemType: 0, +// isRandomItem: 0, +// randomWeight: 0, +// }, +// } +// ) +// .lean(); +// if (!newRandomItem) { +// throw new Error(`Item ${randomItem._id.toString()} was already sold out`); +// } - // 2단계: 유저 정보를 업데이트합니다. - await updateEventStatus(req.userOid, { - ticket1Delta: randomItem.itemType === 1 ? 1 : 0, - ticket2Delta: randomItem.itemType === 2 ? 1 : 0, - }); +// // 2단계: 유저 정보를 업데이트합니다. +// await updateEventStatus(req.userOid, { +// ticket1Delta: randomItem.itemType === 1 ? 1 : 0, +// ticket2Delta: randomItem.itemType === 2 ? 1 : 0, +// }); - // 3단계: Transaction을 추가합니다. - const transaction = new transactionModel({ - type: "use", - amount: 0, - userId: req.userOid, - item: randomItem._id, - itemType: randomItem.itemType, - comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`, - }); - await transaction.save(); +// // 3단계: Transaction을 추가합니다. +// const transaction = new transactionModel({ +// type: "use", +// amount: 0, +// userId: req.userOid, +// itemId: randomItem._id, +// comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`, +// }); +// await transaction.save(); - return newRandomItem; - } catch (err) { - logger.error(err); - logger.warn( - `User ${req.userOid}'s ${depth + 1}th random box failed due to exception` - ); +// return newRandomItem; +// } catch (err) { +// logger.error(err); +// logger.warn( +// `User ${req.userOid}'s ${depth + 1}th random box failed due to exception` +// ); - return await getRandomItem(req, depth + 1); - } -}; +// return await getRandomItem(req, depth + 1); +// } +// }; -const listHandler = async (_, res) => { - try { - const items = await itemModel - .find( - {}, - "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType" - ) - .lean(); - res.json({ items: items.map(hideItemStock) }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Items/List : internal server error" }); - } -}; - -const purchaseHandler = async (req, res) => { +const purchaseItemHandler = async (req, res) => { try { const { itemId } = req.params; - const item = await itemModel.findOne({ _id: itemId }).lean(); + const item = await itemModel.findById(itemId).lean(); if (!item) - return res.status(400).json({ error: "Items/Purchase : invalid Item" }); + return res.status(400).json({ error: "Items/purchase : invalid Item" }); + + const { amount } = req.body; + const totalPrice = item.price * amount; - // 구매 가능 조건: 크레딧이 충분하며, 재고가 남아있으며, 판매 중인 아이템이어야 합니다. + // 구매 가능 조건: 재화가 충분하며, 재고가 남아있으며, 판매 중인 상품이어야 합니다. if (item.isDisabled) - return res.status(400).json({ error: "Items/Purchase : disabled item" }); - if (req.eventStatus.creditAmount < item.price) + return res.status(400).json({ error: "Items/purchase : disabled item" }); + if (req.eventStatus.creditAmount < totalPrice) return res .status(400) - .json({ error: "Items/Purchase : not enough credit" }); - if (item.stock <= 0) + .json({ error: "Items/purchase : not enough credit" }); + if (item.stock < amount) return res .status(400) - .json({ error: "Items/Purchase : item out of stock" }); + .json({ error: "Items/purchase : item out of stock" }); // 1단계: 재고를 차감합니다. const { modifiedCount } = await itemModel.updateOne( - { _id: item._id, stock: { $gt: 0 } }, - { - $inc: { - stock: -1, - }, - } + { _id: item._id, stock: { $gte: amount } }, + { $inc: { stock: -amount } } ); if (modifiedCount === 0) return res .status(400) - .json({ error: "Items/Purchase : item out of stock" }); + .json({ error: "Items/purchase : item out of stock" }); - // 2단계: 유저 정보를 업데이트합니다. - await updateEventStatus(req.userOid, { - creditDelta: -item.price, - ticket1Delta: item.itemType === 1 ? 1 : 0, - ticket2Delta: item.itemType === 2 ? 1 : 0, - }); + if (item.itemType !== 3) { + // 랜덤박스가 아닌 상품을 구입한 경우 + // 2단계: 유저 정보를 업데이트합니다. + await updateEventStatus(req.userOid, { + creditDelta: -totalPrice, + ticket1Delta: item.itemType === 1 ? amount : 0, + ticket2Delta: item.itemType === 2 ? amount : 0, + }); - // 3단계: Transaction을 추가합니다. - const transaction = new transactionModel({ - type: "use", - amount: item.price, - userId: req.userOid, - item: item._id, - itemType: item.itemType, - comment: `${eventConfig?.credit.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, - }); - await transaction.save(); + // 3단계: 출금 내역을 추가합니다. + const transaction = new transactionModel({ + type: "use", + amount: totalPrice, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 사용해 "${item.name}" ${amount}개를 획득했습니다.`, + }); + await transaction.save(); - // 4단계: 랜덤박스인 경우 아이템을 추첨합니다. - if (item.itemType !== 3) return res.json({ result: true }); + // 4단계: 퀘스트를 완료 처리합니다. + await contracts.completeItemPurchaseQuest( + req.userOid, + transaction.createdAt + ); - const randomItem = await getRandomItem(req, 0); - if (!randomItem) { - // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다. - // TODO: Transactions 도입 후 이 코드는 삭제합니다. - logger.info(`User ${req.userOid}'s status will be restored`); + return res.json({ result: true }); + } else { + // 랜덤박스를 구입한 경우 + // 2단계: 대박(40%)인지 쪽박(60%)인지 결정합니다. + const isJackpot = Math.random() < 0.4; + const creditDelta = isJackpot ? totalPrice : -totalPrice; - await transactionModel.deleteOne({ _id: transaction._id }); - await updateEventStatus(req.userOid, { - creditDelta: item.price, - }); - await itemModel.updateOne( - { _id: item._id }, - { - $inc: { - stock: 1, - }, - } - ); + // 3단계: 유저 정보를 업데이트합니다. + await updateEventStatus(req.userOid, { creditDelta }); - logger.info(`User ${req.userOid}'s status was successfully restored`); + // 4단계: 입출금 내역을 추가합니다. + if (isJackpot) { + const transaction = new transactionModel({ + type: "get", + amount: creditDelta, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용해 대박을 터뜨렸습니다.`, + }); + await transaction.save(); + } else { + const transaction = new transactionModel({ + type: "use", + amount: creditDelta, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용했지만 쪽박을 맞았습니다.`, + }); + await transaction.save(); + } - return res - .status(500) - .json({ error: "Items/Purchase : random box error" }); + return res.json({ result: true, isJackpot }); } - res.json({ - result: true, - reward: hideItemStock(randomItem), - }); + // const randomItem = await getRandomItem(req, 0); + // if (!randomItem) { + // // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다. + // // TODO: Transactions 도입 후 이 코드는 삭제합니다. + // logger.info(`User ${req.userOid}'s status will be restored`); + + // await transactionModel.deleteOne({ _id: transaction._id }); + // await updateEventStatus(req.userOid, { + // creditDelta: item.price, + // }); + // await itemModel.updateOne( + // { _id: item._id }, + // { + // $inc: { + // stock: 1, + // }, + // } + // ); + + // logger.info(`User ${req.userOid}'s status was successfully restored`); + + // return res + // .status(500) + // .json({ error: "Items/purchase : random box error" }); + // } } catch (err) { logger.error(err); - res.status(500).json({ error: "Items/Purchase : internal server error" }); + res.status(500).json({ error: "Items/purchase : internal server error" }); } }; module.exports = { - listHandler, - purchaseHandler, + getItemsHandler, + getItemLeaderboardHandler, + purchaseItemHandler, }; diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js index ae1472bc..5a0c6ae6 100644 --- a/src/lottery/services/quests.js +++ b/src/lottery/services/quests.js @@ -3,20 +3,38 @@ const logger = require("../../modules/logger"); const contracts = require("../modules/contracts"); -const completeHandler = async (req, res) => { +const completeQuestHandler = async (req, res) => { try { const quest = contracts.quests[req.params.questId]; if (!quest || !quest.isApiRequired) - return res.status(400).json({ error: "Quests/Complete: invalid Quest" }); + return res.status(400).json({ error: "Quests/complete: invalid quest" }); + + // 출석 체크 퀘스트는 하루에 1번만 완료하도록 제한합니다. + if (quest.id === "dailyAttendance") { + const todayMidnight = new Date(req.timestamp); + todayMidnight.setHours(0, 0, 0, 0); + + const tomorrowMidnight = new Date(todayMidnight); + tomorrowMidnight.setDate(tomorrowMidnight.getDate() + 1); + + // 오늘 완료된 dailyAttendance 퀘스트가 있는지 확인합니다. + const completedQuest = req.eventStatus.completedQuests.find( + ({ questId, completedAt }) => + questId === quest.id && + completedAt >= todayMidnight && + completedAt < tomorrowMidnight + ); + if (completedQuest) return res.json({ result: false }); + } const result = await completeQuest(req.userOid, req.timestamp, quest); res.json({ result: !!result }); // boolean으로 변환하기 위해 !!를 사용합니다. } catch (err) { logger.error(err); - res.status(500).json({ error: "Quests/Complete: internal server error" }); + res.status(500).json({ error: "Quests/complete: internal server error" }); } }; module.exports = { - completeHandler, + completeQuestHandler, }; diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js index 1d920870..fe976e28 100644 --- a/src/lottery/services/transactions.js +++ b/src/lottery/services/transactions.js @@ -1,25 +1,34 @@ const { transactionModel } = require("../modules/stores/mongo"); +const { + transactionPopulateOption, +} = require("../modules/populates/transactions"); const logger = require("../../modules/logger"); -const hideItemStock = (transaction) => { - if (transaction.item) { - transaction.item.stock = transaction.item.stock > 0 ? 1 : 0; +const formatTransaction = (transaction) => { + if (transaction.itemId) { + transaction.item = transaction.itemId; + delete transaction.itemId; } return transaction; }; const getUserTransactionsHandler = async (req, res) => { try { - // userId는 이미 Frontend에서 알고 있고, 중복되는 값이므로 제외합니다. const transactions = await transactionModel - .find({ userId: req.userOid }, "_id type amount questId comment createAt") + .find( + { userId: req.userOid }, + "type amount questId itemId comment createdAt" + ) + .populate(transactionPopulateOption) .lean(); - if (transactions) - res.json({ - transactions, - }); - else - res.status(500).json({ error: "Transactions/ : internal server error" }); + if (!transactions) + return res + .status(500) + .json({ error: "Transactions/ : internal server error" }); + + res.json({ + transactions: transactions.map(formatTransaction), + }); } catch (err) { logger.error(err); res.status(500).json({ error: "Transactions/ : internal server error" }); diff --git a/src/modules/fare.js b/src/modules/fare.js index b2da72a5..350b8207 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -32,8 +32,8 @@ const scaledTime = (time) => { const initializeDatabase = async () => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." @@ -60,7 +60,7 @@ const initializeDatabase = async () => { { fare: true } ) .lean() - ).fare; + )?.fare; const fare = prevTaxiFare ? prevTaxiFare : await callTaxiFare(from, to); @@ -126,8 +126,8 @@ const initializeDatabase = async () => { */ const updateTaxiFare = async (sTime, isMajor) => { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." @@ -145,22 +145,12 @@ const updateTaxiFare = async (sTime, isMajor) => { const to = await locationModel.findOne({ _id: item.to }); await acc; - await callTaxiFare - .catch((err) => { - logger.error(err.message); - }) + await callTaxiFare(from, to) .then(async (fare) => { if (fare) { await taxiFareModel.updateOne( { from: item.from, to: item.to, time: sTime }, - { fare: fare }, - (err, docs) => { - if (err) - logger.error( - "Error occured while updating Taxi Fare document: " + - err.message - ); - } + { fare: fare } ); } }) @@ -179,8 +169,8 @@ const updateTaxiFare = async (sTime, isMajor) => { */ const callTaxiFare = async (from, to) => { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === undefined || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === undefined + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { logger.error( "There is no credential for Naver Map. Taxi Fare functions are disabled." @@ -189,12 +179,7 @@ const callTaxiFare = async (from, to) => { } return ( await axios.get( - `${ - "https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=" + - from.longitude + - "," + - from.latitude - }&goal=${to.longitude + "," + to.latitude}&options=traoptimal`, + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${from.longitude},${from.latitude}}&goal=${to.longitude},${to.latitude}&options=traoptimal`, { headers: naverMapApi } ) ).data.route.traoptimal[0].summary.taxiFare; diff --git a/src/sampleGenerator/sampleData.json b/src/sampleGenerator/sampleData.json index 546812cd..b2e84816 100644 --- a/src/sampleGenerator/sampleData.json +++ b/src/sampleGenerator/sampleData.json @@ -45,8 +45,8 @@ { "koName": "대전복합터미널", "enName": "Daejeon Terminal Complex", - "longitude": 127.350161, - "latitude": 36.362785 + "longitude": 127.436880, + "latitude": 36.349766 }, { "koName": "만년중학교", diff --git a/src/services/fare.js b/src/services/fare.js index 55d1ad33..638faa65 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -21,14 +21,14 @@ const naverMapApi = { const getTaxiFareHandler = async (req, res) => { try { if ( - naverMapApi["X-NCP-APIGW-API-KEY"] === false || - naverMapApi["X-NCP-APIGW-API-KEY-ID"] === false + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] ) { - res.status(503).json({ + return res.status(503).json({ error: "fare/getTaxiFareHandler: Naver Map API credential not found", }); - return; } + const from = await locationModel .findOne({ _id: { $eq: req.query.from }, @@ -40,46 +40,21 @@ const getTaxiFareHandler = async (req, res) => { const sTime = scaledTime(new Date(req.query.time)); if (!from || !to) { - res + return res .status(400) .json({ error: "fare/getTaxiFareHandler: Wrong location" }); - return; + } else if (req.query.from === req.query.to) { + // 프론트엔드에서 예상 택시비를 숨기기 위해 0원을 반환 + return res.status(200).json({ fare: 0 }); } - const isMajor = ( - await taxiFareModel - .findOne( - { from: from._id, to: to._id, time: 0 }, - { isMajor: true }, - (err, docs) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + - err.message - ); - } - ) - .lean() - ).isMajor; - // 시간대별 정보 관리 (현재: 카이스트 본원 <-> 대전역) - if (isMajor) { - const taxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: sTime, - }, - (err, docs) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + - err.message - ); - } - ) - .lean(); + + const fare = await taxiFareModel + .findOne({ from: from._id, to: to._id, time: sTime }) + .lean(); + // 해당 sTime 대로 값이 존재하는 경우 (현재: 카이스트 본원 <-> 대전역) + if (fare) { //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { + if (fare.fare <= 0) { await callTaxiFare(from, to) .then((fare) => { res.status(200).json({ fare: fare }); @@ -88,27 +63,19 @@ const getTaxiFareHandler = async (req, res) => { logger.error(err.message); }); } else { - res.status(200).json({ fare: taxiFare.fare }); + res.status(200).json({ fare: fare.fare }); } } else { - const taxiFare = await taxiFareModel - .findOne( - { - from: from._id, - to: to._id, - time: 0, - }, - (err, docs) => { - if (err) - logger.error( - "Error occured while finding Taxi Fare documents: " + - err.message - ); - } - ) + const minorTaxiFare = await taxiFareModel + .findOne({ + from: from._id, + to: to._id, + time: 48 * new Date(req.query.time).getDay() + 0, + }) .lean(); + //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 - if (!taxiFare || taxiFare.fare <= 0) { + if (!minorTaxiFare || minorTaxiFare.fare <= 0) { await callTaxiFare(from, to) .then((fare) => { res.status(200).json({ fare: fare }); @@ -117,7 +84,7 @@ const getTaxiFareHandler = async (req, res) => { logger.error(err.message); }); } else { - res.status(200).json({ fare: taxiFare.fare }); + res.status(200).json({ fare: minorTaxiFare.fare }); } } } catch (err) { diff --git a/src/services/rooms.js b/src/services/rooms.js index e9beb5fb..d2626d72 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -611,12 +611,7 @@ const commitSettlementHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completePayingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( + await contracts?.completeFareSettlementQuest( req.userOid, req.timestamp, roomObject @@ -689,12 +684,7 @@ const commitPaymentHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completeSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( + await contracts?.completeFarePaymentQuest( req.userOid, req.timestamp, roomObject From 56bd968a9aeae504175290acecc2d0bcad3da07c Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Mon, 2 Sep 2024 23:22:47 +0900 Subject: [PATCH 097/151] Add: modulize validation of service ban --- src/modules/ban.js | 43 +++++++++++++++++++++++++++++++++++++------ src/services/rooms.js | 22 +++++++--------------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/modules/ban.js b/src/modules/ban.js index 290e8f86..9be3aeb2 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -1,7 +1,23 @@ const logger = require("./logger"); const { banModel } = require("./stores/mongo"); -const getMaxValidServiceBanRecord = async (req) => { +/** + * + * @param {*} req + * @param {String} service + */ +const validateServiceBanRecord = async (req, service) => { + switch (service) { + case "Rooms/create": + case "Rooms/join": + var _serviceName = "service"; + break; + default: + logger.error( + "Error occured while validateServiceBanRecord: given service is not defined." + ); + return; + } try { // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 const bans = await banModel.find({ @@ -9,25 +25,40 @@ const getMaxValidServiceBanRecord = async (req) => { expireAt: { $gte: req.timestamp, }, - serviceName: "service", + serviceName: _serviceName, }); + var banRecord = undefined; if (bans.length > 0) { // 가장 expireAt이 큰 정지 기록만 반환함. - const latestBan = bans.reduce( + var banRecord = bans.reduce( (max, ban) => (ban.expireAt > max.expireAt ? ban : max), bans[0] ); - return latestBan; } - return; } catch (err) { logger.error( "Error occured while getValidServiceBanRecord: " + err.message ); return; } + if (banRecord != undefined) { + const formattedExpireAt = banRecord.expireAt + .toISOString() + .replace("T", " ") + .split(".")[0]; + switch (service) { + case "Rooms/create": + var banErrorMessage = `Rooms/create : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from creating rooms until ${formattedExpireAt}.`; + break; + case "Rooms/join": + var banErrorMessage = `Rooms/join : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from joining rooms until ${formattedExpireAt}.`; + break; + } + return banErrorMessage; + } + return; }; module.exports = { - getMaxValidServiceBanRecord, + validateServiceBanRecord, }; diff --git a/src/services/rooms.js b/src/services/rooms.js index d2626d72..495a7dc9 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -21,21 +21,17 @@ const eventPeriod = eventConfig && { endAt: new Date(eventConfig.period.endAt), }; const { contracts } = require("../lottery"); -const { getMaxValidServiceBanRecord } = require("../modules/ban"); +const { validateServiceBanRecord } = require("../modules/ban"); const createHandler = async (req, res) => { const { name, from, to, time, maxPartLength } = req.body; try { // 사용자가 방 생성 기능에 대하여 이용 정지 상태인지 확인합니다. - const banRecord = await getMaxValidServiceBanRecord(req); - if (banRecord != undefined) { - const formattedExpireAt = banRecord.expireAt - .toISOString() - .replace("T", " ") - .split(".")[0]; + const banErrorMessage = await validateServiceBanRecord(req, "Rooms/create"); + if (banErrorMessage != undefined) { return res.status(400).json({ - error: `Rooms/create : user ${req.userId} is temporarily restricted from creating rooms until ${formattedExpireAt}.`, + error: banErrorMessage, }); } @@ -251,14 +247,10 @@ const joinHandler = async (req, res) => { .populate("ongoingRoom"); // 사용자가 방 참여 기능에 대하여 이용 정지 상태인지 확인합니다. - const banRecord = await getMaxValidServiceBanRecord(req); - if (banRecord != undefined) { - const formattedExpireAt = banRecord.expireAt - .toISOString() - .replace("T", " ") - .split(".")[0]; + const banErrorMessage = await validateServiceBanRecord(req, "Rooms/join"); + if (banErrorMessage != undefined) { return res.status(400).json({ - error: `Rooms/join : user ${req.userId} is temporarily restricted from joining rooms until ${formattedExpireAt}.`, + error: banErrorMessage, }); } From 7eceae0e9582385dc031560d353fd57e041cc639 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 3 Sep 2024 21:19:48 +0900 Subject: [PATCH 098/151] Add: resolve comments --- src/modules/ban.js | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/modules/ban.js b/src/modules/ban.js index 9be3aeb2..d86c2a72 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -7,16 +7,19 @@ const { banModel } = require("./stores/mongo"); * @param {String} service */ const validateServiceBanRecord = async (req, service) => { + let _serviceName = undefined; + let banRecord = undefined; switch (service) { case "Rooms/create": case "Rooms/join": - var _serviceName = "service"; + _serviceName = "service"; break; - default: - logger.error( - "Error occured while validateServiceBanRecord: given service is not defined." - ); - return; + } + if (_serviceName === undefined) { + logger.error( + "Error occured while validateServiceBanRecord: given service is not defined." + ); + return; } try { // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 @@ -27,10 +30,9 @@ const validateServiceBanRecord = async (req, service) => { }, serviceName: _serviceName, }); - var banRecord = undefined; if (bans.length > 0) { // 가장 expireAt이 큰 정지 기록만 반환함. - var banRecord = bans.reduce( + banRecord = bans.reduce( (max, ban) => (ban.expireAt > max.expireAt ? ban : max), bans[0] ); @@ -41,19 +43,12 @@ const validateServiceBanRecord = async (req, service) => { ); return; } - if (banRecord != undefined) { + if (banRecord !== undefined) { const formattedExpireAt = banRecord.expireAt .toISOString() .replace("T", " ") .split(".")[0]; - switch (service) { - case "Rooms/create": - var banErrorMessage = `Rooms/create : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from creating rooms until ${formattedExpireAt}.`; - break; - case "Rooms/join": - var banErrorMessage = `Rooms/join : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from joining rooms until ${formattedExpireAt}.`; - break; - } + const banErrorMessage = `${service} : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from service until ${formattedExpireAt}.`; return banErrorMessage; } return; From eecc5f67b66150119f5d9b689d509f84f4811020 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 3 Sep 2024 22:55:43 +0900 Subject: [PATCH 099/151] Add: resolve comments --- src/modules/ban.js | 21 ++++++++++----------- src/services/rooms.js | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/modules/ban.js b/src/modules/ban.js index d86c2a72..939b7d9f 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -23,19 +23,18 @@ const validateServiceBanRecord = async (req, service) => { } try { // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 - const bans = await banModel.find({ - userSid: req.session.loginInfo.sid, - expireAt: { - $gte: req.timestamp, - }, - serviceName: _serviceName, - }); + const bans = await banModel + .find({ + userSid: req.session.loginInfo.sid, + expireAt: { + $gte: req.timestamp, + }, + serviceName: _serviceName, + }) + .sort({ expireAt: -1 }); if (bans.length > 0) { // 가장 expireAt이 큰 정지 기록만 반환함. - banRecord = bans.reduce( - (max, ban) => (ban.expireAt > max.expireAt ? ban : max), - bans[0] - ); + banRecord = bans[0]; } } catch (err) { logger.error( diff --git a/src/services/rooms.js b/src/services/rooms.js index 495a7dc9..0de9064f 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -29,7 +29,7 @@ const createHandler = async (req, res) => { try { // 사용자가 방 생성 기능에 대하여 이용 정지 상태인지 확인합니다. const banErrorMessage = await validateServiceBanRecord(req, "Rooms/create"); - if (banErrorMessage != undefined) { + if (banErrorMessage !== undefined) { return res.status(400).json({ error: banErrorMessage, }); @@ -248,7 +248,7 @@ const joinHandler = async (req, res) => { // 사용자가 방 참여 기능에 대하여 이용 정지 상태인지 확인합니다. const banErrorMessage = await validateServiceBanRecord(req, "Rooms/join"); - if (banErrorMessage != undefined) { + if (banErrorMessage !== undefined) { return res.status(400).json({ error: banErrorMessage, }); From 91516934dff7b1c88da344c15e8c45dc43eb83df Mon Sep 17 00:00:00 2001 From: hwmin414 Date: Tue, 3 Sep 2024 23:11:29 +0900 Subject: [PATCH 100/151] feat: add item handler --- src/lottery/routes/docs/schemas/itemsSchema.js | 3 +++ src/lottery/routes/items.js | 5 +++++ src/lottery/services/items.js | 14 ++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index 7e570b5a..d224ba70 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -3,6 +3,9 @@ const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const { objectId } = require("../../../../modules/patterns"); const itemsZod = { + getItemHandler: z.object({ + itemId: z.string().regex(objectId), + }), getItemLeaderboardHandler: z.object({ itemId: z.string().regex(objectId), }), diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 135617b8..0de8be18 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -6,6 +6,11 @@ const { itemsZod } = require("./docs/schemas/itemsSchema"); const itemsHandlers = require("../services/items"); router.get("/", itemsHandlers.getItemsHandler); +router.get( + "/:itemId", + validateParams(itemsZod.getItemHandler), + itemsHandlers.getItemHandler +); router.get( "/leaderboard/:itemId", validateParams(itemsZod.getItemLeaderboardHandler), diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 9196ad53..94cc5e86 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -25,6 +25,19 @@ const getItemsHandler = async (req, res) => { } }; +const getItemHandler = async (req, res) => { + try { + const { itemId } = req.params; + const item = await itemModel.findById(itemId).lean(); + if (!item) return res.status(400).json({ error: "Items/ : invalid item" }); + + res.json({ item }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Items/ : internal server error" }); + } +}; + // 유도 과정은 services/publicNotice.js 파일에 정의된 calculateProbabilityV2 함수의 주석 참조 const calculateWinProbability = (realStock, users, amount, totalAmount) => { if (users.length <= realStock) return 1; @@ -363,6 +376,7 @@ const purchaseItemHandler = async (req, res) => { module.exports = { getItemsHandler, + getItemHandler, getItemLeaderboardHandler, purchaseItemHandler, }; From ab696255998f5cae7da0bf4ce4d6e520dfd41fa0 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 3 Sep 2024 23:37:54 +0900 Subject: [PATCH 101/151] Add: resolve comments --- src/routes/users.js | 2 +- src/services/users.js | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/routes/users.js b/src/routes/users.js index d5366155..faba91e1 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -57,7 +57,7 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler); router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); // 유저의 현재 유효한 서비스 정지 기록들만 반환합니다. -router.get("/isBanned", userHandlers.isBannedHandler); +router.get("/getValidBanRecord", userHandlers.getValidBanRecordHandler); // 유저의 서비스 정지 기록들을 모두 반환합니다. router.get("/getBanRecord", userHandlers.getBanRecordHandler); diff --git a/src/services/users.js b/src/services/users.js index 5d8ee632..ab73c8e1 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -200,15 +200,17 @@ const resetProfileImgHandler = async (req, res) => { } }; -const isBannedHandler = async (req, res) => { +const getValidBanRecordHandler = async (req, res) => { try { - // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 - const result = await banModel.find({ - userId: req.userOid, - expireAt: { - $gte: req.timestamp, - }, - }); + // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 + const result = await banModel + .find({ + userSid: req.session.loginInfo.sid, + expireAt: { + $gte: req.timestamp, + }, + }) + .sort({ expireAt: -1 }); if (!result) return res.status(500).send("Users/isBanned : internal server error"); res.status(200).json(result); @@ -219,8 +221,12 @@ const isBannedHandler = async (req, res) => { const getBanRecordHandler = async (req, res) => { try { - // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 - const result = await banModel.find({ userId: req.userOid }); + // 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 + const result = await banModel + .find({ + userSid: req.session.loginInfo.sid, + }) + .sort({ expireAt: -1 }); if (!result) return res.status(500).send("Users/getBanRecord : internal server error"); res.status(200).json(result); @@ -238,6 +244,6 @@ module.exports = { editProfileImgDoneHandler, resetNicknameHandler, resetProfileImgHandler, - isBannedHandler, + getValidBanRecordHandler, getBanRecordHandler, }; From 9d47b87a00badfabb8366ac07c9e23691f1ff551 Mon Sep 17 00:00:00 2001 From: hwmin414 Date: Tue, 3 Sep 2024 23:54:15 +0900 Subject: [PATCH 102/151] feat: unused field remove && update doc --- src/lottery/routes/docs/items.js | 82 ++++++++++++++++++++++++++++++++ src/lottery/services/items.js | 8 +++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index 06a6c118..71b866ea 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -82,6 +82,83 @@ itemsDocs[`${apiPrefix}/`] = { }, }, }; +itemsDocs[`${apiPrefix}/:itemId`] = { + get: { + tags: [`${apiPrefix}`], + summary: "상점에서 판매하는 특정 상품의 정보 반환", + description: "상점에서 판매하는 특정 상품의 정보를 가져옵니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "상품 정보를 조회할 ObjectId", + example: "ITEM ID", + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + required: ["items"], + properties: { + item: { + type: "object", + description: "상품의 정보", + properties: { + _id: { + type: "string", + description: "상품의 ObjectId", + example: "ITEM ID", + }, + name: { + type: "string", + description: "상품의 이름", + example: "진짜 송편", + }, + description: { + type: "string", + description: "상품의 설명", + example: "먹을 수 있는 송편입니다.", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "THUMBNAIL URL", + }, + instagramStoryStickerImageUrl: { + type: "string", + description: "인스타그램 스토리 스티커 이미지 URL", + example: "STICKER URL", + }, + price: { + type: "number", + description: "상품의 가격. 0 이상의 정수입니다.", + example: 400, + }, + isDisabled: { + type: "boolean", + description: "상품의 판매 중지 여부", + example: false, + }, + itemType: { + type: "number", + description: + "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", + example: 0, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { get: { tags: [`${apiPrefix}`], @@ -136,6 +213,11 @@ itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { description: "유저가 상품에 당첨될 확률", example: 0.1, }, + rank: { + type: "number", + description: "순위", + example: 1, + }, }, }, }, diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 94cc5e86..eb00535d 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -28,7 +28,12 @@ const getItemsHandler = async (req, res) => { const getItemHandler = async (req, res) => { try { const { itemId } = req.params; - const item = await itemModel.findById(itemId).lean(); + const item = await itemModel + .findById( + itemId, + "_id name description imageUrl instagramStoryStickerImageUrl price isDisabled itemType" + ) + .lean(); if (!item) return res.status(400).json({ error: "Items/ : invalid item" }); res.json({ item }); @@ -126,6 +131,7 @@ const getItemLeaderboardHandler = async (req, res) => { profileImageUrl: userInfo.profileImageUrl, amount: user.amount, probability: user.probability, + rank: user.rank, }; }) ); From dde8965a7b49ea007fc2189ef61b1e97b623a4dc Mon Sep 17 00:00:00 2001 From: hwmin414 Date: Wed, 4 Sep 2024 00:09:40 +0900 Subject: [PATCH 103/151] fix: minor doc update --- src/lottery/routes/docs/items.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index 71b866ea..7f473d15 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -82,7 +82,7 @@ itemsDocs[`${apiPrefix}/`] = { }, }, }; -itemsDocs[`${apiPrefix}/:itemId`] = { +itemsDocs[`${apiPrefix}/{itemId}`] = { get: { tags: [`${apiPrefix}`], summary: "상점에서 판매하는 특정 상품의 정보 반환", @@ -102,7 +102,7 @@ itemsDocs[`${apiPrefix}/:itemId`] = { "application/json": { schema: { type: "object", - required: ["items"], + required: ["item"], properties: { item: { type: "object", From 4d487c6f16210e1ee6206b87476c2a04cdda817b Mon Sep 17 00:00:00 2001 From: hwmin414 Date: Wed, 4 Sep 2024 00:14:46 +0900 Subject: [PATCH 104/151] fix: minor doc fix --- src/lottery/routes/docs/items.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index 7f473d15..063e5f45 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -106,6 +106,15 @@ itemsDocs[`${apiPrefix}/{itemId}`] = { properties: { item: { type: "object", + required: [ + "_id", + "name", + "description", + "imageUrl", + "price", + "isDisabled", + "itemType", + ], description: "상품의 정보", properties: { _id: { @@ -191,6 +200,7 @@ itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { "profileImageUrl", "amount", "probability", + "rank", ], properties: { nickname: { From b341a3395f72dc392d1edc86a9c4b3dba36e3452 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 4 Sep 2024 00:26:41 +0900 Subject: [PATCH 105/151] Add: ban middleware --- src/middlewares/ban.js | 21 +++++++++++++++++++++ src/modules/ban.js | 18 +++--------------- src/routes/rooms.js | 3 +++ src/services/rooms.js | 16 ---------------- 4 files changed, 27 insertions(+), 31 deletions(-) create mode 100644 src/middlewares/ban.js diff --git a/src/middlewares/ban.js b/src/middlewares/ban.js new file mode 100644 index 00000000..ee354ee8 --- /dev/null +++ b/src/middlewares/ban.js @@ -0,0 +1,21 @@ +const { validateServiceBanRecord } = require("../modules/ban"); + +const banMiddleware = async (req, res, next) => { + console.log(`req.originalUrl: ${req.originalUrl}`); + const serviceMapper = { + "/rooms/create": "service", + "/rooms/join": "service", + }; + const banErrorMessage = await validateServiceBanRecord( + req, + serviceMapper[req.originalUrl] + ); + if (banErrorMessage !== undefined) { + console.log("banned user"); + return res.status(400).json({ error: banErrorMessage }); + } + console.log("next()"); + next(); +}; + +module.exports = banMiddleware; diff --git a/src/modules/ban.js b/src/modules/ban.js index 939b7d9f..859a4a33 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -7,20 +7,8 @@ const { banModel } = require("./stores/mongo"); * @param {String} service */ const validateServiceBanRecord = async (req, service) => { - let _serviceName = undefined; let banRecord = undefined; - switch (service) { - case "Rooms/create": - case "Rooms/join": - _serviceName = "service"; - break; - } - if (_serviceName === undefined) { - logger.error( - "Error occured while validateServiceBanRecord: given service is not defined." - ); - return; - } + try { // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 const bans = await banModel @@ -29,7 +17,7 @@ const validateServiceBanRecord = async (req, service) => { expireAt: { $gte: req.timestamp, }, - serviceName: _serviceName, + serviceName: service, }) .sort({ expireAt: -1 }); if (bans.length > 0) { @@ -47,7 +35,7 @@ const validateServiceBanRecord = async (req, service) => { .toISOString() .replace("T", " ") .split(".")[0]; - const banErrorMessage = `${service} : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from service until ${formattedExpireAt}.`; + const banErrorMessage = `${req.originalUrl} : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from service until ${formattedExpireAt}.`; return banErrorMessage; } return; diff --git a/src/routes/rooms.js b/src/routes/rooms.js index 6345fa6f..1076e39b 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -43,6 +43,9 @@ router.get( roomHandlers.infoHandler ); +// 방 생성/참여전 ban 여부 확인 +router.use(require("../middlewares/ban")); + // JSON으로 받은 정보로 방을 생성한다. router.post( "/create", diff --git a/src/services/rooms.js b/src/services/rooms.js index 0de9064f..8b15f72d 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -27,14 +27,6 @@ const createHandler = async (req, res) => { const { name, from, to, time, maxPartLength } = req.body; try { - // 사용자가 방 생성 기능에 대하여 이용 정지 상태인지 확인합니다. - const banErrorMessage = await validateServiceBanRecord(req, "Rooms/create"); - if (banErrorMessage !== undefined) { - return res.status(400).json({ - error: banErrorMessage, - }); - } - if (from === to) { return res.status(400).json({ error: "Rooms/create : locations are same", @@ -246,14 +238,6 @@ const joinHandler = async (req, res) => { .findOne({ id: req.userId }) .populate("ongoingRoom"); - // 사용자가 방 참여 기능에 대하여 이용 정지 상태인지 확인합니다. - const banErrorMessage = await validateServiceBanRecord(req, "Rooms/join"); - if (banErrorMessage !== undefined) { - return res.status(400).json({ - error: banErrorMessage, - }); - } - // 사용자의 참여중인 진행중인 방이 5개 이상이면 오류를 반환합니다. if (user.ongoingRoom.length >= 5) { return res.status(400).json({ From 7dd207396f58af50332626a76b06025475f27a1a Mon Sep 17 00:00:00 2001 From: static Date: Wed, 4 Sep 2024 11:26:22 +0900 Subject: [PATCH 106/151] Add: realStock field in item info endpoint --- src/lottery/routes/docs/items.js | 24 +++++++----------------- src/lottery/services/items.js | 7 ++----- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index 063e5f45..8ca28359 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -23,7 +23,6 @@ itemsDocs[`${apiPrefix}/`] = { required: [ "_id", "name", - "description", "imageUrl", "price", "isDisabled", @@ -40,21 +39,11 @@ itemsDocs[`${apiPrefix}/`] = { description: "상품의 이름", example: "진짜 송편", }, - description: { - type: "string", - description: "상품의 설명", - example: "먹을 수 있는 송편입니다.", - }, imageUrl: { type: "string", description: "상품의 썸네일 이미지 URL", example: "THUMBNAIL URL", }, - instagramStoryStickerImageUrl: { - type: "string", - description: "인스타그램 스토리 스티커 이미지 URL", - example: "STICKER URL", - }, price: { type: "number", description: "상품의 가격. 0 이상의 정수입니다.", @@ -92,7 +81,7 @@ itemsDocs[`${apiPrefix}/{itemId}`] = { in: "path", name: "itemId", required: true, - description: "상품 정보를 조회할 ObjectId", + description: "정보를 조회할 상품의 ObjectId", example: "ITEM ID", }, ], @@ -114,6 +103,7 @@ itemsDocs[`${apiPrefix}/{itemId}`] = { "price", "isDisabled", "itemType", + "realStock", ], description: "상품의 정보", properties: { @@ -137,11 +127,6 @@ itemsDocs[`${apiPrefix}/{itemId}`] = { description: "상품의 썸네일 이미지 URL", example: "THUMBNAIL URL", }, - instagramStoryStickerImageUrl: { - type: "string", - description: "인스타그램 스토리 스티커 이미지 URL", - example: "STICKER URL", - }, price: { type: "number", description: "상품의 가격. 0 이상의 정수입니다.", @@ -158,6 +143,11 @@ itemsDocs[`${apiPrefix}/{itemId}`] = { "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", example: 0, }, + realStock: { + type: "number", + description: "상품의 실제 재고", + example: 30, + }, }, }, }, diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index eb00535d..33c95c58 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -13,10 +13,7 @@ const contracts = require("../modules/contracts"); const getItemsHandler = async (req, res) => { try { const items = await itemModel - .find( - {}, - "_id name description imageUrl instagramStoryStickerImageUrl price isDisabled itemType" - ) + .find({}, "_id name imageUrl price isDisabled itemType") .lean(); res.json({ items }); } catch (err) { @@ -31,7 +28,7 @@ const getItemHandler = async (req, res) => { const item = await itemModel .findById( itemId, - "_id name description imageUrl instagramStoryStickerImageUrl price isDisabled itemType" + "_id name description imageUrl price isDisabled itemType realStock" ) .lean(); if (!item) return res.status(400).json({ error: "Items/ : invalid item" }); From 12f9bcf65c1ddba14276c67517a554f9f3591a4d Mon Sep 17 00:00:00 2001 From: static Date: Wed, 4 Sep 2024 11:35:50 +0900 Subject: [PATCH 107/151] Fix: quest can be achieved infinitely --- src/lottery/modules/quests.js | 2 +- src/lottery/routes/docs/globalState.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 0d79ac81..6bf162fb 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -85,7 +85,7 @@ const completeQuest = async (userId, timestamp, quest) => { // 3단계: 유저의 퀘스트 완료 횟수를 확인합니다. // maxCount가 0인 경우, 무제한으로 퀘스트를 완료할 수 있습니다. const questCount = eventStatus.completedQuests.filter( - (completedQuestId) => completedQuestId === quest.id + ({ questId }) => questId === quest.id ).length; if (quest.maxCount > 0 && questCount >= quest.maxCount) { logger.info( diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 1bbf23f4..44b62384 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -103,9 +103,9 @@ globalStateDocs[`${apiPrefix}/`] = { "유저가 완료한 퀘스트의 배열. 여러 번 완료한 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.", items: { type: "object", - required: ["id", "completedAt"], + required: ["questId", "completedAt"], properties: { - id: { + questId: { type: "string", description: "퀘스트의 Id", example: "QUEST ID", From 074d3572a33f309e1890026457ac2eb1666e066a Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 4 Sep 2024 15:16:55 +0900 Subject: [PATCH 108/151] Add: resolve comments --- src/middlewares/ban.js | 13 +++++-------- src/routes/rooms.js | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/middlewares/ban.js b/src/middlewares/ban.js index ee354ee8..bd4fd39f 100644 --- a/src/middlewares/ban.js +++ b/src/middlewares/ban.js @@ -1,20 +1,17 @@ const { validateServiceBanRecord } = require("../modules/ban"); +const serviceMapper = new Map([ + ["/rooms/create", "service"], + ["/rooms/join", "service"], +]); const banMiddleware = async (req, res, next) => { - console.log(`req.originalUrl: ${req.originalUrl}`); - const serviceMapper = { - "/rooms/create": "service", - "/rooms/join": "service", - }; const banErrorMessage = await validateServiceBanRecord( req, - serviceMapper[req.originalUrl] + serviceMapper.get(req.originalUrl) ); if (banErrorMessage !== undefined) { - console.log("banned user"); return res.status(400).json({ error: banErrorMessage }); } - console.log("next()"); next(); }; diff --git a/src/routes/rooms.js b/src/routes/rooms.js index 1076e39b..b8bad634 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -35,6 +35,9 @@ router.get( // 이후 API 접근 시 로그인 필요 router.use(require("../middlewares/auth")); +// 방 생성/참여전 ban 여부 확인 +router.use(require("../middlewares/ban")); + // 특정 id 방 세부사항 보기 router.get( "/info", @@ -43,9 +46,6 @@ router.get( roomHandlers.infoHandler ); -// 방 생성/참여전 ban 여부 확인 -router.use(require("../middlewares/ban")); - // JSON으로 받은 정보로 방을 생성한다. router.post( "/create", From 8da0c4aed63967226a21a509bea2f21b5ffc6f8a Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 4 Sep 2024 15:51:51 +0900 Subject: [PATCH 109/151] Add: resolve comments --- src/middlewares/ban.js | 1 + src/modules/ban.js | 3 +- src/routes/docs/users.js | 67 ++-------------------------------------- src/routes/users.js | 3 -- src/services/users.js | 20 ------------ 5 files changed, 4 insertions(+), 90 deletions(-) diff --git a/src/middlewares/ban.js b/src/middlewares/ban.js index bd4fd39f..f70b9550 100644 --- a/src/middlewares/ban.js +++ b/src/middlewares/ban.js @@ -1,4 +1,5 @@ const { validateServiceBanRecord } = require("../modules/ban"); + const serviceMapper = new Map([ ["/rooms/create", "service"], ["/rooms/join", "service"], diff --git a/src/modules/ban.js b/src/modules/ban.js index 859a4a33..4068db78 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -2,7 +2,6 @@ const logger = require("./logger"); const { banModel } = require("./stores/mongo"); /** - * * @param {*} req * @param {String} service */ @@ -26,7 +25,7 @@ const validateServiceBanRecord = async (req, service) => { } } catch (err) { logger.error( - "Error occured while getValidServiceBanRecord: " + err.message + "Error occured while validateServiceBanRecord: " + err.message ); return; } diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index ec4532a5..0f19ecde 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -330,7 +330,7 @@ usersDocs[`${apiPrefix}/resetProfileImg`] = { }, }; -usersDocs[`${apiPrefix}/isBanned`] = { +usersDocs[`${apiPrefix}/getBanRecord`] = { get: { tags: [tag], summary: "본인의 현재 정지 기록을 가져움", @@ -344,7 +344,7 @@ usersDocs[`${apiPrefix}/isBanned`] = { type: "array", items: { properties: { - userId: { + userSid: { type: "string", description: "사용자의 SSO ID", pattern: "monday-sid", @@ -375,69 +375,6 @@ usersDocs[`${apiPrefix}/isBanned`] = { }, }, }, - 400: { - content: { - "text/html": { - example: "Users/isBanned : there is no ban record", - }, - }, - }, - 500: { - content: { - "text/html": { - example: "Users/isBanned : internal server error", - }, - }, - }, - }, - }, -}; - -usersDocs[`${apiPrefix}/getBanRecord`] = { - get: { - tags: [tag], - summary: "본인의 모든 정지 기록을 가져움", - description: - "정지 기록들 중 본인인 경우에 해당하는 정지 기록을 모두 가져옴", - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "array", - items: { - properties: { - userId: { - type: "string", - description: "사용자의 SSO ID", - example: "monday-sid", - }, - reason: { - type: "string", - description: "정지 사유", - example: "미정산", - }, - bannedAt: { - type: "date", - description: "정지 당한 시각", - example: "2024-05-20 12:00", - }, - expireAt: { - type: "date", - description: "정지 만료 시각", - example: "2024-05-21 12:00", - }, - serviceName: { - type: "string", - description: "정지를 당한 서비스 또는 이벤트 이름", - example: "2023-fall-event", - }, - }, - }, - }, - }, - }, - }, 400: { content: { "text/html": { diff --git a/src/routes/users.js b/src/routes/users.js index faba91e1..de3c0b1d 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -56,9 +56,6 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler); // 프로필 이미지를 기본값으로 재설정합니다. router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); -// 유저의 현재 유효한 서비스 정지 기록들만 반환합니다. -router.get("/getValidBanRecord", userHandlers.getValidBanRecordHandler); - // 유저의 서비스 정지 기록들을 모두 반환합니다. router.get("/getBanRecord", userHandlers.getBanRecordHandler); diff --git a/src/services/users.js b/src/services/users.js index ab73c8e1..8c51c736 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -200,25 +200,6 @@ const resetProfileImgHandler = async (req, res) => { } }; -const getValidBanRecordHandler = async (req, res) => { - try { - // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 - const result = await banModel - .find({ - userSid: req.session.loginInfo.sid, - expireAt: { - $gte: req.timestamp, - }, - }) - .sort({ expireAt: -1 }); - if (!result) - return res.status(500).send("Users/isBanned : internal server error"); - res.status(200).json(result); - } catch (err) { - res.status(500).send("Users/isBanned : internal server error"); - } -}; - const getBanRecordHandler = async (req, res) => { try { // 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 @@ -244,6 +225,5 @@ module.exports = { editProfileImgDoneHandler, resetNicknameHandler, resetProfileImgHandler, - getValidBanRecordHandler, getBanRecordHandler, }; From 0729f66180fa7572812ef2288aadb1593774faf8 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 4 Sep 2024 15:58:09 +0900 Subject: [PATCH 110/151] Remove: unnecessary require --- src/services/rooms.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/rooms.js b/src/services/rooms.js index 8b15f72d..0ef798fe 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -21,7 +21,6 @@ const eventPeriod = eventConfig && { endAt: new Date(eventConfig.period.endAt), }; const { contracts } = require("../lottery"); -const { validateServiceBanRecord } = require("../modules/ban"); const createHandler = async (req, res) => { const { name, from, to, time, maxPartLength } = req.body; From eb051110bf71552be49e170baa838ddf88092538 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 5 Sep 2024 01:10:42 +0900 Subject: [PATCH 111/151] Fix: validation error when random box result is jjokbak --- src/lottery/services/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 33c95c58..cffd28f7 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -334,7 +334,7 @@ const purchaseItemHandler = async (req, res) => { } else { const transaction = new transactionModel({ type: "use", - amount: creditDelta, + amount: -creditDelta, userId: req.userOid, itemId: item._id, itemAmount: amount, From ec52dff6c01ae8a4a8aa2615825d738aede9ba00 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 5 Sep 2024 01:22:38 +0900 Subject: [PATCH 112/151] Refactor: use same response format in two querying item info endpoints --- src/lottery/routes/docs/items.js | 12 ++++++++++++ src/lottery/services/items.js | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index 8ca28359..28ecd53d 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -23,10 +23,12 @@ itemsDocs[`${apiPrefix}/`] = { required: [ "_id", "name", + "description", "imageUrl", "price", "isDisabled", "itemType", + "realStock", ], properties: { _id: { @@ -39,6 +41,11 @@ itemsDocs[`${apiPrefix}/`] = { description: "상품의 이름", example: "진짜 송편", }, + description: { + type: "string", + description: "상품의 설명", + example: "먹을 수 있는 송편입니다.", + }, imageUrl: { type: "string", description: "상품의 썸네일 이미지 URL", @@ -60,6 +67,11 @@ itemsDocs[`${apiPrefix}/`] = { "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", example: 0, }, + realStock: { + type: "number", + description: "상품의 실제 재고", + example: 30, + }, }, }, }, diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index cffd28f7..07730574 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -13,7 +13,10 @@ const contracts = require("../modules/contracts"); const getItemsHandler = async (req, res) => { try { const items = await itemModel - .find({}, "_id name imageUrl price isDisabled itemType") + .find( + {}, + "_id name description imageUrl price isDisabled itemType realStock" + ) .lean(); res.json({ items }); } catch (err) { From 69565147a776ec2eae227ff76c0a9f6afaeb0e0b Mon Sep 17 00:00:00 2001 From: static Date: Sat, 7 Sep 2024 04:55:22 +0900 Subject: [PATCH 113/151] Refactor: update quest information --- src/lottery/modules/contracts.js | 46 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index a415b0af..6281bec7 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -15,15 +15,15 @@ const quests = buildQuests({ description: "이벤트 참여만 해도 송편코인을 얻을 수 있다고?? 이벤트 참여에 동의하고 송편코인을 받아 보세요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstLogin.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_firstLogin.png", reward: 200, }, firstRoomCreation: { name: "첫 방 개설", description: - "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해보세요.", + "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해 보세요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstRoomCreation.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_firstRoomCreation.png", reward: 500, }, roomSharing: { @@ -31,7 +31,7 @@ const quests = buildQuests({ description: "방을 공유해 친구들을 택시팟에 초대해 보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_roomSharing.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_roomSharing.png", reward: 500, isApiRequired: true, }, @@ -40,64 +40,68 @@ const quests = buildQuests({ description: "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산을 요청해 보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 + 버튼을 눌러 찾을 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_paying.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_fareSettlement.png", reward: 2000, maxCount: 0, }, farePayment: { name: "송금 완료면 I am 신뢰에요", description: - "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", + "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해 주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 + 버튼을 눌러 찾을 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_sending.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_farePayment.png", reward: 2000, maxCount: 0, }, nicknameChanging: { name: "닉네임 폼 미쳤다", description: - "닉네임을 변경하여 자신을 표현하세요. 마이페이지수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", + "닉네임을 변경하여 자신을 표현하세요. 마이 페이지수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_nicknameChanging.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_nicknameChanging.png", reward: 500, }, accountChanging: { name: "계좌 등록을 해야 능률이 올라갑니다", description: - "정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.", + "정산하기 기능을 더욱 빠르게 이용할 수 있다고? 계좌 번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이 페이지수정하기 버튼을 눌러 계좌 번호를 등록 또는 수정할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_accountChanging.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_accountChanging.png", reward: 500, }, adPushAgreement: { name: "Taxi의 소울메이트", description: - "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 때 알려드릴 수 있어요.", + "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 Taxi 앱 사용자가 있을 때 알려드릴 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_adPushAgreement.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_adPushAgreement.png", reward: 500, }, eventSharing: { name: "Taxi를 아십니까", - description: "내가 초대한 사람이 이벤트에 참여하면 송편코인을 드려요.", + description: + "내가 초대한 사람이 이벤트에 참여하면 송편코인을 드려요. 다른 사람의 초대를 받아 이벤트에 참여한 경우에도 이 퀘스트가 달성돼요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_eventSharing.png", reward: 700, maxCount: 0, }, dailyAttendance: { - name: "하루 한 번 Taxi!", + name: "매일매일 출석 췤!", description: - "매일 Taxi에 접속하여 출석 체크를 하면 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요. 송편코인을 얻으려면 출석 체크 페이지에서 출석 버튼을 눌러야 해요.", - imageUrl: "", + "매일 Taxi에 접속하면 하루 한 번 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요.", + imageUrl: + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_dailyAttendance.png", reward: 700, maxCount: 17, isApiRequired: true, }, itemPurchase: { - name: "itemPurchase", - description: "itemPurchase", - imageUrl: "", + name: "Taxi에서 산 응모권", + description: + "응모권 교환소에서 아무 경품 응모권이나 구매해 보세요. Taxi에서 판매하는 응모권은 모두 정품이니 안심해도 좋아요.", + imageUrl: + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_itemPurchase.png", reward: 500, }, }); From 20d5a642254d3e5156ea8f81448797a7880be87f Mon Sep 17 00:00:00 2001 From: static Date: Wed, 25 Sep 2024 00:57:55 +0900 Subject: [PATCH 114/151] Fix: deprecated function and invalid default icon url --- package.json | 2 +- pnpm-lock.yaml | 1525 +++++++++++++++++++++++--------------------- src/modules/fcm.js | 7 +- 3 files changed, 808 insertions(+), 726 deletions(-) diff --git a/package.json b/package.json index 4ea2a648..47d8047c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "express-rate-limit": "^7.1.0", "express-session": "^1.17.3", "express-validator": "^6.14.0", - "firebase-admin": "^11.4.1", + "firebase-admin": "^11.11.1", "jsonwebtoken": "^9.0.2", "mongoose": "^6.12.0", "node-cron": "3.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db4e122d..25c9f218 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: dependencies: '@adminjs/express': specifier: ^5.1.0 - version: 5.1.0(adminjs@6.8.7)(express-formidable@1.2.0)(express-session@1.17.3)(express@4.18.2)(tslib@2.6.2) + version: 5.1.0(adminjs@6.8.7)(express-formidable@1.2.0)(express-session@1.17.3)(express@4.18.2)(tslib@2.7.0) '@adminjs/mongoose': specifier: ^3.0.3 version: 3.0.3(adminjs@6.8.7)(mongoose@6.12.0) @@ -60,14 +60,14 @@ dependencies: specifier: ^6.14.0 version: 6.15.0 firebase-admin: - specifier: ^11.4.1 - version: 11.10.1 + specifier: ^11.11.1 + version: 11.11.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 mongoose: specifier: ^6.12.0 - version: 6.12.0 + version: 6.12.0(@aws-sdk/client-sso-oidc@3.654.0) node-cron: specifier: 3.0.2 version: 3.0.2 @@ -123,7 +123,7 @@ devDependencies: version: 10.2.0 mongodb: specifier: ^4.1.0 - version: 4.17.1 + version: 4.17.1(@aws-sdk/client-sso-oidc@3.654.0) nodemon: specifier: ^3.0.1 version: 3.0.1 @@ -184,7 +184,7 @@ packages: - prop-types dev: false - /@adminjs/express@5.1.0(adminjs@6.8.7)(express-formidable@1.2.0)(express-session@1.17.3)(express@4.18.2)(tslib@2.6.2): + /@adminjs/express@5.1.0(adminjs@6.8.7)(express-formidable@1.2.0)(express-session@1.17.3)(express@4.18.2)(tslib@2.7.0): resolution: {integrity: sha512-+mrtDmoAYA9R+/FTYWOLL48g005yrgcAWC2phdwqGzznIxGKSp2YERcfzdTI7Svtnlaal72/QW8Q3OhzJjVLzQ==} peerDependencies: adminjs: '>=6.0.0' @@ -198,7 +198,7 @@ packages: express-formidable: 1.2.0 express-session: 1.17.3 path-to-regexp: 6.2.1 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@adminjs/mongoose@3.0.3(adminjs@6.8.7)(mongoose@6.12.0): @@ -210,7 +210,7 @@ packages: adminjs: 6.8.7 escape-regexp: 0.0.1 lodash: 4.17.21 - mongoose: 6.12.0 + mongoose: 6.12.0(@aws-sdk/client-sso-oidc@3.654.0) dev: false /@ampproject/remapping@2.2.1: @@ -221,504 +221,527 @@ packages: '@jridgewell/trace-mapping': 0.3.18 dev: false - /@aws-crypto/crc32@3.0.0: - resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + /@aws-crypto/sha256-browser@5.2.0: + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} requiresBuild: true dependencies: - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.425.0 - tslib: 1.14.1 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.654.0 + '@aws-sdk/util-locate-window': 3.568.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.2 optional: true - /@aws-crypto/ie11-detection@3.0.0: - resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + /@aws-crypto/sha256-js@5.2.0: + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - tslib: 1.14.1 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.654.0 + tslib: 2.6.2 optional: true - /@aws-crypto/sha256-browser@3.0.0: - resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + /@aws-crypto/supports-web-crypto@5.2.0: + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} requiresBuild: true dependencies: - '@aws-crypto/ie11-detection': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-crypto/supports-web-crypto': 3.0.0 - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.425.0 - '@aws-sdk/util-locate-window': 3.310.0 - '@aws-sdk/util-utf8-browser': 3.259.0 - tslib: 1.14.1 + tslib: 2.6.2 optional: true - /@aws-crypto/sha256-js@3.0.0: - resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + /@aws-crypto/util@5.2.0: + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} requiresBuild: true dependencies: - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.425.0 - tslib: 1.14.1 + '@aws-sdk/types': 3.654.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.2 optional: true - /@aws-crypto/supports-web-crypto@3.0.0: - resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} - requiresBuild: true - dependencies: - tslib: 1.14.1 + /@aws-sdk/client-cognito-identity@3.654.0: + resolution: {integrity: sha512-3K806KJVivVP011R7Wf4ujGKP8R6d7KFlo9t0Swr9YFnStCdSdjmRX1yW8RpzSzRC4xyuUw+bo8wPf+tE/YxnA==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.654.0(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/client-sts': 3.654.0 + '@aws-sdk/core': 3.654.0 + '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/middleware-host-header': 3.654.0 + '@aws-sdk/middleware-logger': 3.654.0 + '@aws-sdk/middleware-recursion-detection': 3.654.0 + '@aws-sdk/middleware-user-agent': 3.654.0 + '@aws-sdk/region-config-resolver': 3.654.0 + '@aws-sdk/types': 3.654.0 + '@aws-sdk/util-endpoints': 3.654.0 + '@aws-sdk/util-user-agent-browser': 3.654.0 + '@aws-sdk/util-user-agent-node': 3.654.0 + '@smithy/config-resolver': 3.0.8 + '@smithy/core': 2.4.5 + '@smithy/fetch-http-handler': 3.2.8 + '@smithy/hash-node': 3.0.6 + '@smithy/invalid-dependency': 3.0.6 + '@smithy/middleware-content-length': 3.0.8 + '@smithy/middleware-endpoint': 3.1.3 + '@smithy/middleware-retry': 3.0.20 + '@smithy/middleware-serde': 3.0.6 + '@smithy/middleware-stack': 3.0.6 + '@smithy/node-config-provider': 3.1.7 + '@smithy/node-http-handler': 3.2.3 + '@smithy/protocol-http': 4.1.3 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/url-parser': 3.0.6 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.20 + '@smithy/util-defaults-mode-node': 3.0.20 + '@smithy/util-endpoints': 2.1.2 + '@smithy/util-middleware': 3.0.6 + '@smithy/util-retry': 3.0.6 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt optional: true - /@aws-crypto/util@3.0.0: - resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} - requiresBuild: true - dependencies: - '@aws-sdk/types': 3.425.0 - '@aws-sdk/util-utf8-browser': 3.259.0 - tslib: 1.14.1 + /@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0): + resolution: {integrity: sha512-gbHrKsEnaAtmkNCVQzLyiqMzpDaThV/bWl/ODEklI+t6stW3Pe3oDMstEHLfJ6JU5g8sYnx4VLuxlnJMtUkvPw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.654.0 + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sts': 3.654.0 + '@aws-sdk/core': 3.654.0 + '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/middleware-host-header': 3.654.0 + '@aws-sdk/middleware-logger': 3.654.0 + '@aws-sdk/middleware-recursion-detection': 3.654.0 + '@aws-sdk/middleware-user-agent': 3.654.0 + '@aws-sdk/region-config-resolver': 3.654.0 + '@aws-sdk/types': 3.654.0 + '@aws-sdk/util-endpoints': 3.654.0 + '@aws-sdk/util-user-agent-browser': 3.654.0 + '@aws-sdk/util-user-agent-node': 3.654.0 + '@smithy/config-resolver': 3.0.8 + '@smithy/core': 2.4.5 + '@smithy/fetch-http-handler': 3.2.8 + '@smithy/hash-node': 3.0.6 + '@smithy/invalid-dependency': 3.0.6 + '@smithy/middleware-content-length': 3.0.8 + '@smithy/middleware-endpoint': 3.1.3 + '@smithy/middleware-retry': 3.0.20 + '@smithy/middleware-serde': 3.0.6 + '@smithy/middleware-stack': 3.0.6 + '@smithy/node-config-provider': 3.1.7 + '@smithy/node-http-handler': 3.2.3 + '@smithy/protocol-http': 4.1.3 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/url-parser': 3.0.6 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.20 + '@smithy/util-defaults-mode-node': 3.0.20 + '@smithy/util-endpoints': 2.1.2 + '@smithy/util-middleware': 3.0.6 + '@smithy/util-retry': 3.0.6 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt optional: true - /@aws-sdk/client-cognito-identity@3.427.0: - resolution: {integrity: sha512-9brRaNnl6haE7R3R43A5CSNw0k1YtB3xjuArbMg/p6NDUpvRSRgOVNWu2R02Yjh/j2ZuaLOCPLuCipb+PHQPKQ==} - engines: {node: '>=14.0.0'} - requiresBuild: true - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.427.0 - '@aws-sdk/credential-provider-node': 3.427.0 - '@aws-sdk/middleware-host-header': 3.425.0 - '@aws-sdk/middleware-logger': 3.425.0 - '@aws-sdk/middleware-recursion-detection': 3.425.0 - '@aws-sdk/middleware-signing': 3.425.0 - '@aws-sdk/middleware-user-agent': 3.427.0 - '@aws-sdk/region-config-resolver': 3.425.0 - '@aws-sdk/types': 3.425.0 - '@aws-sdk/util-endpoints': 3.427.0 - '@aws-sdk/util-user-agent-browser': 3.425.0 - '@aws-sdk/util-user-agent-node': 3.425.0 - '@smithy/config-resolver': 2.0.14 - '@smithy/fetch-http-handler': 2.2.2 - '@smithy/hash-node': 2.0.11 - '@smithy/invalid-dependency': 2.0.11 - '@smithy/middleware-content-length': 2.0.13 - '@smithy/middleware-endpoint': 2.0.11 - '@smithy/middleware-retry': 2.0.16 - '@smithy/middleware-serde': 2.0.11 - '@smithy/middleware-stack': 2.0.5 - '@smithy/node-config-provider': 2.1.1 - '@smithy/node-http-handler': 2.1.7 - '@smithy/protocol-http': 3.0.7 - '@smithy/smithy-client': 2.1.10 - '@smithy/types': 2.3.5 - '@smithy/url-parser': 2.0.11 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.14 - '@smithy/util-defaults-mode-node': 2.0.18 - '@smithy/util-retry': 2.0.4 - '@smithy/util-utf8': 2.0.0 + /@aws-sdk/client-sso@3.654.0: + resolution: {integrity: sha512-4kBxs2IzCDtj6a6lRXa/lXK5wWpMGzwKtb+HMXf/rJYVM6x7wYRzc1hYrOd3DYkFQ/sR3dUFj+0mTP0os3aAbA==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.654.0 + '@aws-sdk/middleware-host-header': 3.654.0 + '@aws-sdk/middleware-logger': 3.654.0 + '@aws-sdk/middleware-recursion-detection': 3.654.0 + '@aws-sdk/middleware-user-agent': 3.654.0 + '@aws-sdk/region-config-resolver': 3.654.0 + '@aws-sdk/types': 3.654.0 + '@aws-sdk/util-endpoints': 3.654.0 + '@aws-sdk/util-user-agent-browser': 3.654.0 + '@aws-sdk/util-user-agent-node': 3.654.0 + '@smithy/config-resolver': 3.0.8 + '@smithy/core': 2.4.5 + '@smithy/fetch-http-handler': 3.2.8 + '@smithy/hash-node': 3.0.6 + '@smithy/invalid-dependency': 3.0.6 + '@smithy/middleware-content-length': 3.0.8 + '@smithy/middleware-endpoint': 3.1.3 + '@smithy/middleware-retry': 3.0.20 + '@smithy/middleware-serde': 3.0.6 + '@smithy/middleware-stack': 3.0.6 + '@smithy/node-config-provider': 3.1.7 + '@smithy/node-http-handler': 3.2.3 + '@smithy/protocol-http': 4.1.3 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/url-parser': 3.0.6 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.20 + '@smithy/util-defaults-mode-node': 3.0.20 + '@smithy/util-endpoints': 2.1.2 + '@smithy/util-middleware': 3.0.6 + '@smithy/util-retry': 3.0.6 + '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 transitivePeerDependencies: - aws-crt optional: true - /@aws-sdk/client-sso@3.427.0: - resolution: {integrity: sha512-sFVFEmsQ1rmgYO1SgrOTxE/MTKpeE4hpOkm1WqhLQK7Ij136vXpjCxjH1JYZiHiUzO1wr9t4ex4dlB5J3VS/Xg==} - engines: {node: '>=14.0.0'} - requiresBuild: true - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/middleware-host-header': 3.425.0 - '@aws-sdk/middleware-logger': 3.425.0 - '@aws-sdk/middleware-recursion-detection': 3.425.0 - '@aws-sdk/middleware-user-agent': 3.427.0 - '@aws-sdk/region-config-resolver': 3.425.0 - '@aws-sdk/types': 3.425.0 - '@aws-sdk/util-endpoints': 3.427.0 - '@aws-sdk/util-user-agent-browser': 3.425.0 - '@aws-sdk/util-user-agent-node': 3.425.0 - '@smithy/config-resolver': 2.0.14 - '@smithy/fetch-http-handler': 2.2.2 - '@smithy/hash-node': 2.0.11 - '@smithy/invalid-dependency': 2.0.11 - '@smithy/middleware-content-length': 2.0.13 - '@smithy/middleware-endpoint': 2.0.11 - '@smithy/middleware-retry': 2.0.16 - '@smithy/middleware-serde': 2.0.11 - '@smithy/middleware-stack': 2.0.5 - '@smithy/node-config-provider': 2.1.1 - '@smithy/node-http-handler': 2.1.7 - '@smithy/protocol-http': 3.0.7 - '@smithy/smithy-client': 2.1.10 - '@smithy/types': 2.3.5 - '@smithy/url-parser': 2.0.11 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.14 - '@smithy/util-defaults-mode-node': 2.0.18 - '@smithy/util-retry': 2.0.4 - '@smithy/util-utf8': 2.0.0 + /@aws-sdk/client-sts@3.654.0: + resolution: {integrity: sha512-tyHa8jsBy+/NQZFHm6Q2Q09Vi9p3EH4yPy6PU8yPewpi2klreObtrUd0anJa6nzjS9SSuqnlZWsRic3cQ4QwCg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.654.0(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/core': 3.654.0 + '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/middleware-host-header': 3.654.0 + '@aws-sdk/middleware-logger': 3.654.0 + '@aws-sdk/middleware-recursion-detection': 3.654.0 + '@aws-sdk/middleware-user-agent': 3.654.0 + '@aws-sdk/region-config-resolver': 3.654.0 + '@aws-sdk/types': 3.654.0 + '@aws-sdk/util-endpoints': 3.654.0 + '@aws-sdk/util-user-agent-browser': 3.654.0 + '@aws-sdk/util-user-agent-node': 3.654.0 + '@smithy/config-resolver': 3.0.8 + '@smithy/core': 2.4.5 + '@smithy/fetch-http-handler': 3.2.8 + '@smithy/hash-node': 3.0.6 + '@smithy/invalid-dependency': 3.0.6 + '@smithy/middleware-content-length': 3.0.8 + '@smithy/middleware-endpoint': 3.1.3 + '@smithy/middleware-retry': 3.0.20 + '@smithy/middleware-serde': 3.0.6 + '@smithy/middleware-stack': 3.0.6 + '@smithy/node-config-provider': 3.1.7 + '@smithy/node-http-handler': 3.2.3 + '@smithy/protocol-http': 4.1.3 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/url-parser': 3.0.6 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.20 + '@smithy/util-defaults-mode-node': 3.0.20 + '@smithy/util-endpoints': 2.1.2 + '@smithy/util-middleware': 3.0.6 + '@smithy/util-retry': 3.0.6 + '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 transitivePeerDependencies: - aws-crt optional: true - /@aws-sdk/client-sts@3.427.0: - resolution: {integrity: sha512-le2wLJKILyWuRfPz2HbyaNtu5kEki+ojUkTqCU6FPDRrqUvEkaaCBH9Awo/2AtrCfRkiobop8RuTTj6cAnpiJg==} - engines: {node: '>=14.0.0'} + /@aws-sdk/core@3.654.0: + resolution: {integrity: sha512-4Rwx7BVaNaFqmXBDmnOkMbyuIFFbpZ+ru4lr660p45zY1QoNNSalechfoRffcokLFOZO+VWEJkdcorPUUU993w==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/credential-provider-node': 3.427.0 - '@aws-sdk/middleware-host-header': 3.425.0 - '@aws-sdk/middleware-logger': 3.425.0 - '@aws-sdk/middleware-recursion-detection': 3.425.0 - '@aws-sdk/middleware-sdk-sts': 3.425.0 - '@aws-sdk/middleware-signing': 3.425.0 - '@aws-sdk/middleware-user-agent': 3.427.0 - '@aws-sdk/region-config-resolver': 3.425.0 - '@aws-sdk/types': 3.425.0 - '@aws-sdk/util-endpoints': 3.427.0 - '@aws-sdk/util-user-agent-browser': 3.425.0 - '@aws-sdk/util-user-agent-node': 3.425.0 - '@smithy/config-resolver': 2.0.14 - '@smithy/fetch-http-handler': 2.2.2 - '@smithy/hash-node': 2.0.11 - '@smithy/invalid-dependency': 2.0.11 - '@smithy/middleware-content-length': 2.0.13 - '@smithy/middleware-endpoint': 2.0.11 - '@smithy/middleware-retry': 2.0.16 - '@smithy/middleware-serde': 2.0.11 - '@smithy/middleware-stack': 2.0.5 - '@smithy/node-config-provider': 2.1.1 - '@smithy/node-http-handler': 2.1.7 - '@smithy/protocol-http': 3.0.7 - '@smithy/smithy-client': 2.1.10 - '@smithy/types': 2.3.5 - '@smithy/url-parser': 2.0.11 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.14 - '@smithy/util-defaults-mode-node': 2.0.18 - '@smithy/util-retry': 2.0.4 - '@smithy/util-utf8': 2.0.0 - fast-xml-parser: 4.2.5 + '@smithy/core': 2.4.5 + '@smithy/node-config-provider': 3.1.7 + '@smithy/property-provider': 3.1.6 + '@smithy/protocol-http': 4.1.3 + '@smithy/signature-v4': 4.1.4 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/util-middleware': 3.0.6 + fast-xml-parser: 4.4.1 tslib: 2.6.2 - transitivePeerDependencies: - - aws-crt optional: true - /@aws-sdk/credential-provider-cognito-identity@3.427.0: - resolution: {integrity: sha512-BQNzNrMJlBAfXhYNdAUqaVASpT9Aho5swj7glZKxx4Uds1w5Pih2e14JWgnl8XgUWAZ36pchTrV1aA4JT7N8vw==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-cognito-identity@3.654.0: + resolution: {integrity: sha512-0aq4Ri9VYjixS7AZKNmuJc/5MlQdfrkgtzHV1TBisoroi/ed1WWnZmQvUFi3ZqRkt1Cvi7oZi6J1gZEfzq8p8g==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/client-cognito-identity': 3.427.0 - '@aws-sdk/types': 3.425.0 - '@smithy/property-provider': 2.0.12 - '@smithy/types': 2.3.5 + '@aws-sdk/client-cognito-identity': 3.654.0 + '@aws-sdk/types': 3.654.0 + '@smithy/property-provider': 3.1.6 + '@smithy/types': 3.4.2 tslib: 2.6.2 transitivePeerDependencies: - aws-crt optional: true - /@aws-sdk/credential-provider-env@3.425.0: - resolution: {integrity: sha512-J20etnLvMKXRVi5FK4F8yOCNm2RTaQn5psQTGdDEPWJNGxohcSpzzls8U2KcMyUJ+vItlrThr4qwgpHG3i/N0w==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-env@3.654.0: + resolution: {integrity: sha512-kogsx3Ql81JouHS7DkheCDU9MYAvK0AokxjcshDveGmf7BbgbWCA8Fnb9wjQyNDaOXNvkZu8Z8rgkX91z324/w==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/property-provider': 2.0.12 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/property-provider': 3.1.6 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/credential-provider-http@3.425.0: - resolution: {integrity: sha512-aP9nkoVWf+OlNMecrUqe4+RuQrX13nucVbty0HTvuwfwJJj0T6ByWZzle+fo1D+5OxvJmtzTflBWt6jUERdHWA==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-http@3.654.0: + resolution: {integrity: sha512-tgmAH4MBi/aDR882lfw48+tDV95ZH3GWc1Eoe6DpNLiM3GN2VfU/cZwuHmi6aq+vAbdIlswBHJ/+va0fOvlyjw==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/fetch-http-handler': 2.2.2 - '@smithy/node-http-handler': 2.1.7 - '@smithy/property-provider': 2.0.12 - '@smithy/protocol-http': 3.0.7 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/fetch-http-handler': 3.2.8 + '@smithy/node-http-handler': 3.2.3 + '@smithy/property-provider': 3.1.6 + '@smithy/protocol-http': 4.1.3 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/util-stream': 3.1.8 tslib: 2.6.2 optional: true - /@aws-sdk/credential-provider-ini@3.427.0: - resolution: {integrity: sha512-NmH1cO/w98CKMltYec3IrJIIco19wRjATFNiw83c+FGXZ+InJwReqBnruxIOmKTx2KDzd6fwU1HOewS7UjaaaQ==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-ini@3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0): + resolution: {integrity: sha512-DKSdaNu2hwdmuvnm9KnA0NLqMWxxmxSOLWjSUSoFIm++wGXUjPrRMFYKvMktaXnPuyf5my8gF/yGbwzPZ8wlTg==} + engines: {node: '>=16.0.0'} requiresBuild: true + peerDependencies: + '@aws-sdk/client-sts': ^3.654.0 dependencies: - '@aws-sdk/credential-provider-env': 3.425.0 - '@aws-sdk/credential-provider-process': 3.425.0 - '@aws-sdk/credential-provider-sso': 3.427.0 - '@aws-sdk/credential-provider-web-identity': 3.425.0 - '@aws-sdk/types': 3.425.0 - '@smithy/credential-provider-imds': 2.0.16 - '@smithy/property-provider': 2.0.12 - '@smithy/shared-ini-file-loader': 2.2.0 - '@smithy/types': 2.3.5 + '@aws-sdk/client-sts': 3.654.0 + '@aws-sdk/credential-provider-env': 3.654.0 + '@aws-sdk/credential-provider-http': 3.654.0 + '@aws-sdk/credential-provider-process': 3.654.0 + '@aws-sdk/credential-provider-sso': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0) + '@aws-sdk/credential-provider-web-identity': 3.654.0(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/types': 3.654.0 + '@smithy/credential-provider-imds': 3.2.3 + '@smithy/property-provider': 3.1.6 + '@smithy/shared-ini-file-loader': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt optional: true - /@aws-sdk/credential-provider-node@3.427.0: - resolution: {integrity: sha512-wYYbQ57nKL8OfgRbl8k6uXcdnYml+p3LSSfDUAuUEp1HKlQ8lOXFJ3BdLr5qrk7LhpyppSRnWBmh2c3kWa7ANQ==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-node@3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0): + resolution: {integrity: sha512-wPV7CNYaXDEc+SS+3R0v8SZwkHRUE1z2k2j1d49tH5QBDT4tb/k2V/biXWkwSk3hbR+IMWXmuhJDv/5lybhIvg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/credential-provider-env': 3.425.0 - '@aws-sdk/credential-provider-ini': 3.427.0 - '@aws-sdk/credential-provider-process': 3.425.0 - '@aws-sdk/credential-provider-sso': 3.427.0 - '@aws-sdk/credential-provider-web-identity': 3.425.0 - '@aws-sdk/types': 3.425.0 - '@smithy/credential-provider-imds': 2.0.16 - '@smithy/property-provider': 2.0.12 - '@smithy/shared-ini-file-loader': 2.2.0 - '@smithy/types': 2.3.5 + '@aws-sdk/credential-provider-env': 3.654.0 + '@aws-sdk/credential-provider-http': 3.654.0 + '@aws-sdk/credential-provider-ini': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/credential-provider-process': 3.654.0 + '@aws-sdk/credential-provider-sso': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0) + '@aws-sdk/credential-provider-web-identity': 3.654.0(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/types': 3.654.0 + '@smithy/credential-provider-imds': 3.2.3 + '@smithy/property-provider': 3.1.6 + '@smithy/shared-ini-file-loader': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' - aws-crt optional: true - /@aws-sdk/credential-provider-process@3.425.0: - resolution: {integrity: sha512-YY6tkLdvtb1Fgofp3b1UWO+5vwS14LJ/smGmuGpSba0V7gFJRdcrJ9bcb9vVgAGuMdjzRJ+bUKlLLtqXkaykEw==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-process@3.654.0: + resolution: {integrity: sha512-PmQoo8sZ9Q2Ow8OMzK++Z9lI7MsRUG7sNq3E72DVA215dhtTICTDQwGlXH2AAmIp7n+G9LLRds+4wo2ehG4mkg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/property-provider': 2.0.12 - '@smithy/shared-ini-file-loader': 2.2.0 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/property-provider': 3.1.6 + '@smithy/shared-ini-file-loader': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/credential-provider-sso@3.427.0: - resolution: {integrity: sha512-c+tXyS/i49erHs4bAp6vKNYeYlyQ0VNMBgoco0LCn1rL0REtHbfhWMnqDLF6c2n3yIWDOTrQu0D73Idnpy16eA==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-sso@3.654.0(@aws-sdk/client-sso-oidc@3.654.0): + resolution: {integrity: sha512-7GFme6fWEdA/XYKzZPOAdj/jS6fMBy1NdSIZsDXikS0v9jU+ZzHrAaWt13YLzHyjgxB9Sg9id9ncdY1IiubQXQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/client-sso': 3.427.0 - '@aws-sdk/token-providers': 3.427.0 - '@aws-sdk/types': 3.425.0 - '@smithy/property-provider': 2.0.12 - '@smithy/shared-ini-file-loader': 2.2.0 - '@smithy/types': 2.3.5 + '@aws-sdk/client-sso': 3.654.0 + '@aws-sdk/token-providers': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0) + '@aws-sdk/types': 3.654.0 + '@smithy/property-provider': 3.1.6 + '@smithy/shared-ini-file-loader': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt optional: true - /@aws-sdk/credential-provider-web-identity@3.425.0: - resolution: {integrity: sha512-/0R65TgRzL01JU3SzloivWNwdkbIhr06uY/F5pBHf/DynQqaspKNfdHn6AiozgSVDfwRHFjKBTUy6wvf3QFkuA==} - engines: {node: '>=14.0.0'} + /@aws-sdk/credential-provider-web-identity@3.654.0(@aws-sdk/client-sts@3.654.0): + resolution: {integrity: sha512-6a2g9gMtZToqSu+CusjNK5zvbLJahQ9di7buO3iXgbizXpLXU1rnawCpWxwslMpT5fLgMSKDnKDrr6wdEk7jSw==} + engines: {node: '>=16.0.0'} requiresBuild: true + peerDependencies: + '@aws-sdk/client-sts': ^3.654.0 dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/property-provider': 2.0.12 - '@smithy/types': 2.3.5 + '@aws-sdk/client-sts': 3.654.0 + '@aws-sdk/types': 3.654.0 + '@smithy/property-provider': 3.1.6 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/credential-providers@3.427.0: - resolution: {integrity: sha512-rKKohSHju462vo+uQnPjcEZPBAfAMgGH6K1XyyCNpuOC0yYLkG87PYpvAQeb8riTrkHPX0dYUHuTHZ6zQgMGjA==} - engines: {node: '>=14.0.0'} - requiresBuild: true - dependencies: - '@aws-sdk/client-cognito-identity': 3.427.0 - '@aws-sdk/client-sso': 3.427.0 - '@aws-sdk/client-sts': 3.427.0 - '@aws-sdk/credential-provider-cognito-identity': 3.427.0 - '@aws-sdk/credential-provider-env': 3.425.0 - '@aws-sdk/credential-provider-http': 3.425.0 - '@aws-sdk/credential-provider-ini': 3.427.0 - '@aws-sdk/credential-provider-node': 3.427.0 - '@aws-sdk/credential-provider-process': 3.425.0 - '@aws-sdk/credential-provider-sso': 3.427.0 - '@aws-sdk/credential-provider-web-identity': 3.425.0 - '@aws-sdk/types': 3.425.0 - '@smithy/credential-provider-imds': 2.0.16 - '@smithy/property-provider': 2.0.12 - '@smithy/types': 2.3.5 + /@aws-sdk/credential-providers@3.654.0(@aws-sdk/client-sso-oidc@3.654.0): + resolution: {integrity: sha512-e9ZDKnmXOMOQW9e3RQyaLUcerZFzHCickRSPoSxAsGKnrhH/ltIm9Od3uyVILl1TGJoOCxVDMBE9nPfl+vNRzQ==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + '@aws-sdk/client-cognito-identity': 3.654.0 + '@aws-sdk/client-sso': 3.654.0 + '@aws-sdk/client-sts': 3.654.0 + '@aws-sdk/credential-provider-cognito-identity': 3.654.0 + '@aws-sdk/credential-provider-env': 3.654.0 + '@aws-sdk/credential-provider-http': 3.654.0 + '@aws-sdk/credential-provider-ini': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0)(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/credential-provider-process': 3.654.0 + '@aws-sdk/credential-provider-sso': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0) + '@aws-sdk/credential-provider-web-identity': 3.654.0(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/types': 3.654.0 + '@smithy/credential-provider-imds': 3.2.3 + '@smithy/property-provider': 3.1.6 + '@smithy/types': 3.4.2 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt optional: true - /@aws-sdk/middleware-host-header@3.425.0: - resolution: {integrity: sha512-E5Gt41LObQ+cr8QnLthwsH3MtVSNXy1AKJMowDr85h0vzqA/FHUkgHyOGntgozzjXT5M0MaSRYxS0xwTR5D4Ew==} - engines: {node: '>=14.0.0'} - requiresBuild: true - dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/protocol-http': 3.0.7 - '@smithy/types': 2.3.5 - tslib: 2.6.2 - optional: true - - /@aws-sdk/middleware-logger@3.425.0: - resolution: {integrity: sha512-INE9XWRXx2f4a/r2vOU0tAmgctVp7nEaEasemNtVBYhqbKLZvr9ndLBSgKGgJ8LIcXAoISipaMuFiqIGkFsm7A==} - engines: {node: '>=14.0.0'} + /@aws-sdk/middleware-host-header@3.654.0: + resolution: {integrity: sha512-rxGgVHWKp8U2ubMv+t+vlIk7QYUaRCHaVpmUlJv0Wv6Q0KeO9a42T9FxHphjOTlCGQOLcjCreL9CF8Qhtb4mdQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/protocol-http': 4.1.3 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/middleware-recursion-detection@3.425.0: - resolution: {integrity: sha512-77gnzJ5b91bgD75L/ugpOyerx6lR3oyS4080X1YI58EzdyBMkDrHM4FbMcY2RynETi3lwXCFzLRyZjWXY1mRlw==} - engines: {node: '>=14.0.0'} + /@aws-sdk/middleware-logger@3.654.0: + resolution: {integrity: sha512-OQYb+nWlmASyXfRb989pwkJ9EVUMP1CrKn2eyTk3usl20JZmKo2Vjis6I0tLUkMSxMhnBJJlQKyWkRpD/u1FVg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/protocol-http': 3.0.7 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/middleware-sdk-sts@3.425.0: - resolution: {integrity: sha512-JFojrg76oKAoBknnr9EL5N2aJ1mRCtBqXoZYST58GSx8uYdFQ89qS65VNQ8JviBXzsrCNAn4vDhZ5Ch5E6TxGQ==} - engines: {node: '>=14.0.0'} + /@aws-sdk/middleware-recursion-detection@3.654.0: + resolution: {integrity: sha512-gKSomgltKVmsT8sC6W7CrADZ4GHwX9epk3GcH6QhebVO3LA9LRbkL3TwOPUXakxxOLLUTYdOZLIOtFf7iH00lg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/middleware-signing': 3.425.0 - '@aws-sdk/types': 3.425.0 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/protocol-http': 4.1.3 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/middleware-signing@3.425.0: - resolution: {integrity: sha512-ZpOfgJHk7ovQ0sSwg3tU4NxFOnz53lJlkJRf7S+wxQALHM0P2MJ6LYBrZaFMVsKiJxNIdZBXD6jclgHg72ZW6Q==} - engines: {node: '>=14.0.0'} + /@aws-sdk/middleware-user-agent@3.654.0: + resolution: {integrity: sha512-liCcqPAyRsr53cy2tYu4qeH4MMN0eh9g6k56XzI5xd4SghXH5YWh4qOYAlQ8T66ZV4nPMtD8GLtLXGzsH8moFg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/property-provider': 2.0.12 - '@smithy/protocol-http': 3.0.7 - '@smithy/signature-v4': 2.0.11 - '@smithy/types': 2.3.5 - '@smithy/util-middleware': 2.0.4 + '@aws-sdk/types': 3.654.0 + '@aws-sdk/util-endpoints': 3.654.0 + '@smithy/protocol-http': 4.1.3 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/middleware-user-agent@3.427.0: - resolution: {integrity: sha512-y9HxYsNvnA3KqDl8w1jHeCwz4P9CuBEtu/G+KYffLeAMBsMZmh4SIkFFCO9wE/dyYg6+yo07rYcnnIfy7WA0bw==} - engines: {node: '>=14.0.0'} + /@aws-sdk/region-config-resolver@3.654.0: + resolution: {integrity: sha512-ydGOrXJxj3x0sJhsXyTmvJVLAE0xxuTWFJihTl67RtaO7VRNtd82I3P3bwoMMaDn5WpmV5mPo8fEUDRlBm3fPg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@aws-sdk/util-endpoints': 3.427.0 - '@smithy/protocol-http': 3.0.7 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/node-config-provider': 3.1.7 + '@smithy/types': 3.4.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.6 tslib: 2.6.2 optional: true - /@aws-sdk/region-config-resolver@3.425.0: - resolution: {integrity: sha512-u7uv/iUOapIJdRgRkO3wnpYsUgV6ponsZJQgVg/8L+n+Vo5PQL5gAcIuAOwcYSKQPFaeK+KbmByI4SyOK203Vw==} - engines: {node: '>=14.0.0'} + /@aws-sdk/token-providers@3.654.0(@aws-sdk/client-sso-oidc@3.654.0): + resolution: {integrity: sha512-D8GeJYmvbfWkQDtTB4owmIobSMexZel0fOoetwvgCQ/7L8VPph3Q2bn1TRRIXvH7wdt6DcDxA3tKMHPBkT3GlA==} + engines: {node: '>=16.0.0'} requiresBuild: true + peerDependencies: + '@aws-sdk/client-sso-oidc': ^3.654.0 dependencies: - '@smithy/node-config-provider': 2.1.1 - '@smithy/types': 2.3.5 - '@smithy/util-config-provider': 2.0.0 - '@smithy/util-middleware': 2.0.4 + '@aws-sdk/client-sso-oidc': 3.654.0(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/types': 3.654.0 + '@smithy/property-provider': 3.1.6 + '@smithy/shared-ini-file-loader': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@aws-sdk/token-providers@3.427.0: - resolution: {integrity: sha512-4E5E+4p8lJ69PBY400dJXF06LUHYx5lkKzBEsYqWWhoZcoftrvi24ltIhUDoGVLkrLcTHZIWSdFAWSos4hXqeg==} - engines: {node: '>=14.0.0'} + /@aws-sdk/types@3.654.0: + resolution: {integrity: sha512-VWvbED3SV+10QJIcmU/PKjsKilsTV16d1I7/on4bvD/jo1qGeMXqLDBSen3ks/tuvXZF/mFc7ZW/W2DiLVtO7A==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/middleware-host-header': 3.425.0 - '@aws-sdk/middleware-logger': 3.425.0 - '@aws-sdk/middleware-recursion-detection': 3.425.0 - '@aws-sdk/middleware-user-agent': 3.427.0 - '@aws-sdk/types': 3.425.0 - '@aws-sdk/util-endpoints': 3.427.0 - '@aws-sdk/util-user-agent-browser': 3.425.0 - '@aws-sdk/util-user-agent-node': 3.425.0 - '@smithy/config-resolver': 2.0.14 - '@smithy/fetch-http-handler': 2.2.2 - '@smithy/hash-node': 2.0.11 - '@smithy/invalid-dependency': 2.0.11 - '@smithy/middleware-content-length': 2.0.13 - '@smithy/middleware-endpoint': 2.0.11 - '@smithy/middleware-retry': 2.0.16 - '@smithy/middleware-serde': 2.0.11 - '@smithy/middleware-stack': 2.0.5 - '@smithy/node-config-provider': 2.1.1 - '@smithy/node-http-handler': 2.1.7 - '@smithy/property-provider': 2.0.12 - '@smithy/protocol-http': 3.0.7 - '@smithy/shared-ini-file-loader': 2.2.0 - '@smithy/smithy-client': 2.1.10 - '@smithy/types': 2.3.5 - '@smithy/url-parser': 2.0.11 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.14 - '@smithy/util-defaults-mode-node': 2.0.18 - '@smithy/util-retry': 2.0.4 - '@smithy/util-utf8': 2.0.0 + '@smithy/types': 3.4.2 tslib: 2.6.2 - transitivePeerDependencies: - - aws-crt optional: true - /@aws-sdk/types@3.425.0: - resolution: {integrity: sha512-6lqbmorwerN4v+J5dqbHPAsjynI0mkEF+blf+69QTaKKGaxBBVaXgqoqul9RXYcK5MMrrYRbQIMd0zYOoy90kA==} - engines: {node: '>=14.0.0'} + /@aws-sdk/util-endpoints@3.654.0: + resolution: {integrity: sha512-i902fcBknHs0Irgdpi62+QMvzxE+bczvILXigYrlHL4+PiEnlMVpni5L5W1qCkNZXf8AaMrSBuR1NZAGp6UOUw==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/types': 3.4.2 + '@smithy/util-endpoints': 2.1.2 tslib: 2.6.2 optional: true - /@aws-sdk/util-endpoints@3.427.0: - resolution: {integrity: sha512-rSyiAIFF/EVvity/+LWUqoTMJ0a25RAc9iqx0WZ4tf1UjuEXRRXxZEb+jEZg1bk+pY84gdLdx9z5E+MSJCZxNQ==} - engines: {node: '>=14.0.0'} + /@aws-sdk/util-locate-window@3.568.0: + resolution: {integrity: sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/node-config-provider': 2.1.1 tslib: 2.6.2 optional: true - /@aws-sdk/util-locate-window@3.310.0: - resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} - engines: {node: '>=14.0.0'} + /@aws-sdk/util-user-agent-browser@3.654.0: + resolution: {integrity: sha512-ykYAJqvnxLt7wfrqya28wuH3/7NdrwzfiFd7NqEVQf7dXVxL5RPEpD7DxjcyQo3DsHvvdUvGZVaQhozycn1pzA==} requiresBuild: true dependencies: - tslib: 2.6.2 - optional: true - - /@aws-sdk/util-user-agent-browser@3.425.0: - resolution: {integrity: sha512-22Y9iMtjGcFjGILR6/xdp1qRezlHVLyXtnpEsbuPTiernRCPk6zfAnK/ATH77r02MUjU057tdxVkd5umUBTn9Q==} - requiresBuild: true - dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/types': 2.3.5 + '@aws-sdk/types': 3.654.0 + '@smithy/types': 3.4.2 bowser: 2.11.0 tslib: 2.6.2 optional: true - /@aws-sdk/util-user-agent-node@3.425.0: - resolution: {integrity: sha512-SIR4F5uQeeVAi8lv4OgRirtdtNi5zeyogTuQgGi9su8F/WP1N6JqxofcwpUY5f8/oJ2UlXr/tx1f09UHfJJzvA==} - engines: {node: '>=14.0.0'} + /@aws-sdk/util-user-agent-node@3.654.0: + resolution: {integrity: sha512-a0ojjdBN6pqv6gB4H/QPPSfhs7mFtlVwnmKCM/QrTaFzN0U810PJ1BST3lBx5sa23I5jWHGaoFY+5q65C3clLQ==} + engines: {node: '>=16.0.0'} requiresBuild: true peerDependencies: aws-crt: '>=1.0.0' @@ -726,16 +749,9 @@ packages: aws-crt: optional: true dependencies: - '@aws-sdk/types': 3.425.0 - '@smithy/node-config-provider': 2.1.1 - '@smithy/types': 2.3.5 - tslib: 2.6.2 - optional: true - - /@aws-sdk/util-utf8-browser@3.259.0: - resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} - requiresBuild: true - dependencies: + '@aws-sdk/types': 3.654.0 + '@smithy/node-config-provider': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true @@ -2244,7 +2260,7 @@ packages: resolution: {integrity: sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==} dependencies: '@firebase/util': 1.9.3 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@firebase/database-compat@0.3.4: @@ -2255,7 +2271,7 @@ packages: '@firebase/database-types': 0.10.4 '@firebase/logger': 0.4.0 '@firebase/util': 1.9.3 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@firebase/database-types@0.10.4: @@ -2273,19 +2289,19 @@ packages: '@firebase/logger': 0.4.0 '@firebase/util': 1.9.3 faye-websocket: 0.11.4 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@firebase/logger@0.4.0: resolution: {integrity: sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@firebase/util@1.9.3: resolution: {integrity: sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@floating-ui/core@1.4.1: @@ -2313,7 +2329,7 @@ packages: fast-deep-equal: 3.1.3 functional-red-black-tree: 1.0.1 google-gax: 3.6.1 - protobufjs: 7.2.5 + protobufjs: 7.4.0 transitivePeerDependencies: - encoding - supports-color @@ -2355,10 +2371,10 @@ packages: abort-controller: 3.0.0 async-retry: 1.3.3 compressible: 2.0.18 - duplexify: 4.1.2 - ent: 2.2.0 + duplexify: 4.1.3 + ent: 2.2.1 extend: 3.0.2 - fast-xml-parser: 4.2.7 + fast-xml-parser: 4.4.1 gaxios: 5.1.3 google-auth-library: 8.9.0 mime: 3.0.0 @@ -2373,26 +2389,25 @@ packages: dev: false optional: true - /@grpc/grpc-js@1.8.21: - resolution: {integrity: sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg==} + /@grpc/grpc-js@1.8.22: + resolution: {integrity: sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==} engines: {node: ^8.13.0 || >=10.10.0} requiresBuild: true dependencies: - '@grpc/proto-loader': 0.7.8 - '@types/node': 20.4.7 + '@grpc/proto-loader': 0.7.13 + '@types/node': 22.6.1 dev: false optional: true - /@grpc/proto-loader@0.7.8: - resolution: {integrity: sha512-GU12e2c8dmdXb7XUlOgYWZ2o2i+z9/VeACkxTA/zzAe2IjclC5PnVL0lpgjhrqfpDYHzM8B1TF6pqWegMYAzlA==} + /@grpc/proto-loader@0.7.13: + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} engines: {node: '>=6'} hasBin: true requiresBuild: true dependencies: - '@types/long': 4.0.2 lodash.camelcase: 4.3.0 - long: 4.0.0 - protobufjs: 7.2.5 + long: 5.2.3 + protobufjs: 7.4.0 yargs: 17.7.2 dev: false optional: true @@ -2487,8 +2502,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: false - /@jsdoc/salty@0.2.5: - resolution: {integrity: sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==} + /@jsdoc/salty@0.2.8: + resolution: {integrity: sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==} engines: {node: '>=v12.0.0'} requiresBuild: true dependencies: @@ -2496,8 +2511,8 @@ packages: dev: false optional: true - /@mongodb-js/saslprep@1.1.0: - resolution: {integrity: sha512-Xfijy7HvfzzqiOAhAepF4SGN5e9leLkMvg/OPOF97XemjfVCYN/oWa75wnkc6mltMSTwY+XlbhWgUOJmkFspSw==} + /@mongodb-js/saslprep@1.1.9: + resolution: {integrity: sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==} requiresBuild: true dependencies: sparse-bitfield: 3.0.3 @@ -2749,385 +2764,434 @@ packages: rollup: 2.79.1 dev: false - /@smithy/abort-controller@2.0.11: - resolution: {integrity: sha512-MSzE1qR2JNyb7ot3blIOT3O3H0Jn06iNDEgHRaqZUwBgx5EG+VIx24Y21tlKofzYryIOcWpIohLrIIyocD6LMA==} - engines: {node: '>=14.0.0'} + /@smithy/abort-controller@3.1.4: + resolution: {integrity: sha512-VupaALAQlXViW3/enTf/f5l5JZYSAxoJL7f0nanhNNKnww6DGCg1oYIuNP78KDugnkwthBO6iEcym16HhWV8RQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/config-resolver@2.0.14: - resolution: {integrity: sha512-K1K+FuWQoy8j/G7lAmK85o03O89s2Vvh6kMFmzEmiHUoQCRH1rzbDtMnGNiaMHeSeYJ6y79IyTusdRG+LuWwtg==} - engines: {node: '>=14.0.0'} + /@smithy/config-resolver@3.0.8: + resolution: {integrity: sha512-Tv1obAC18XOd2OnDAjSWmmthzx6Pdeh63FbLin8MlPiuJ2ATpKkq0NcNOJFr0dO+JmZXnwu8FQxKJ3TKJ3Hulw==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/node-config-provider': 2.1.1 - '@smithy/types': 2.3.5 - '@smithy/util-config-provider': 2.0.0 - '@smithy/util-middleware': 2.0.4 + '@smithy/node-config-provider': 3.1.7 + '@smithy/types': 3.4.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.6 tslib: 2.6.2 optional: true - /@smithy/credential-provider-imds@2.0.16: - resolution: {integrity: sha512-tKa2xF+69TvGxJT+lnJpGrKxUuAZDLYXFhqnPEgnHz+psTpkpcB4QRjHj63+uj83KaeFJdTfW201eLZeRn6FfA==} - engines: {node: '>=14.0.0'} + /@smithy/core@2.4.5: + resolution: {integrity: sha512-Z0qlPXgZ0pouYgnu/cZTEYeRAvniiKZmVl4wIbZHX/nEMHkMDV9ao6KFArsU9KndE0TuhL149xcRx45wfw1YCA==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/node-config-provider': 2.1.1 - '@smithy/property-provider': 2.0.12 - '@smithy/types': 2.3.5 - '@smithy/url-parser': 2.0.11 + '@smithy/middleware-endpoint': 3.1.3 + '@smithy/middleware-retry': 3.0.20 + '@smithy/middleware-serde': 3.0.6 + '@smithy/protocol-http': 4.1.3 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-middleware': 3.0.6 + '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 optional: true - /@smithy/eventstream-codec@2.0.11: - resolution: {integrity: sha512-BQCTjxhCYRZIfXapa2LmZSaH8QUBGwMZw7XRN83hrdixbLjIcj+o549zjkedFS07Ve2TlvWUI6BTzP+nv7snBA==} + /@smithy/credential-provider-imds@3.2.3: + resolution: {integrity: sha512-VoxMzSzdvkkjMJNE38yQgx4CfnmT+Z+5EUXkg4x7yag93eQkVQgZvN3XBSHC/ylfBbLbAtdu7flTCChX9I+mVg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@aws-crypto/crc32': 3.0.0 - '@smithy/types': 2.3.5 - '@smithy/util-hex-encoding': 2.0.0 + '@smithy/node-config-provider': 3.1.7 + '@smithy/property-provider': 3.1.6 + '@smithy/types': 3.4.2 + '@smithy/url-parser': 3.0.6 tslib: 2.6.2 optional: true - /@smithy/fetch-http-handler@2.2.2: - resolution: {integrity: sha512-K7aRtRuaBjzlk+jWWeyfDTLAmRRvmA4fU8eHUXtjsuEDgi3f356ZE32VD2ssxIH13RCLVZbXMt5h7wHzYiSuVA==} + /@smithy/fetch-http-handler@3.2.8: + resolution: {integrity: sha512-Lqe0B8F5RM7zkw//6avq1SJ8AfaRd3ubFUS1eVp5WszV7p6Ne5hQ4dSuMHDpNRPhgTvj4va9Kd/pcVigHEHRow==} requiresBuild: true dependencies: - '@smithy/protocol-http': 3.0.7 - '@smithy/querystring-builder': 2.0.11 - '@smithy/types': 2.3.5 - '@smithy/util-base64': 2.0.0 + '@smithy/protocol-http': 4.1.3 + '@smithy/querystring-builder': 3.0.6 + '@smithy/types': 3.4.2 + '@smithy/util-base64': 3.0.0 tslib: 2.6.2 optional: true - /@smithy/hash-node@2.0.11: - resolution: {integrity: sha512-PbleVugN2tbhl1ZoNWVrZ1oTFFas/Hq+s6zGO8B9bv4w/StTriTKA9W+xZJACOj9X7zwfoTLbscM+avCB1KqOQ==} - engines: {node: '>=14.0.0'} + /@smithy/hash-node@3.0.6: + resolution: {integrity: sha512-c/FHEdKK/7DU2z6ZE91L36ahyXWayR3B+FzELjnYq7wH5YqIseM24V+pWCS9kFn1Ln8OFGTf+pyYPiHZuX0s/Q==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 - '@smithy/util-buffer-from': 2.0.0 - '@smithy/util-utf8': 2.0.0 + '@smithy/types': 3.4.2 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 optional: true - /@smithy/invalid-dependency@2.0.11: - resolution: {integrity: sha512-zazq99ujxYv/NOf9zh7xXbNgzoVLsqE0wle8P/1zU/XdhPi/0zohTPKWUzIxjGdqb5hkkwfBkNkl5H+LE0mvgw==} + /@smithy/invalid-dependency@3.0.6: + resolution: {integrity: sha512-czM7Ioq3s8pIXht7oD+vmgy4Wfb4XavU/k/irO8NdXFFOx7YAlsCCcKOh/lJD1mJSYQqiR7NmpZ9JviryD/7AQ==} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/is-array-buffer@2.0.0: - resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} + /@smithy/is-array-buffer@2.2.0: + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} requiresBuild: true dependencies: tslib: 2.6.2 optional: true - /@smithy/middleware-content-length@2.0.13: - resolution: {integrity: sha512-Md2kxWpaec3bXp1oERFPQPBhOXCkGSAF7uc1E+4rkwjgw3/tqAXRtbjbggu67HJdwaif76As8AV6XxbD1HzqTQ==} - engines: {node: '>=14.0.0'} + /@smithy/is-array-buffer@3.0.0: + resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/protocol-http': 3.0.7 - '@smithy/types': 2.3.5 tslib: 2.6.2 optional: true - /@smithy/middleware-endpoint@2.0.11: - resolution: {integrity: sha512-mCugsvB15up6fqpzUEpMT4CuJmFkEI+KcozA7QMzYguXCaIilyMKsyxgamwmr+o7lo3QdjN0//XLQ9bWFL129g==} - engines: {node: '>=14.0.0'} + /@smithy/middleware-content-length@3.0.8: + resolution: {integrity: sha512-VuyszlSO49WKh3H9/kIO2kf07VUwGV80QRiaDxUfP8P8UKlokz381ETJvwLhwuypBYhLymCYyNhB3fLAGBX2og==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/middleware-serde': 2.0.11 - '@smithy/types': 2.3.5 - '@smithy/url-parser': 2.0.11 - '@smithy/util-middleware': 2.0.4 + '@smithy/protocol-http': 4.1.3 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/middleware-retry@2.0.16: - resolution: {integrity: sha512-Br5+0yoiMS0ugiOAfJxregzMMGIRCbX4PYo1kDHtLgvkA/d++aHbnHB819m5zOIAMPvPE7AThZgcsoK+WOsUTA==} - engines: {node: '>=14.0.0'} + /@smithy/middleware-endpoint@3.1.3: + resolution: {integrity: sha512-KeM/OrK8MVFUsoJsmCN0MZMVPjKKLudn13xpgwIMpGTYpA8QZB2Xq5tJ+RE6iu3A6NhOI4VajDTwBsm8pwwrhg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/node-config-provider': 2.1.1 - '@smithy/protocol-http': 3.0.7 - '@smithy/service-error-classification': 2.0.4 - '@smithy/types': 2.3.5 - '@smithy/util-middleware': 2.0.4 - '@smithy/util-retry': 2.0.4 + '@smithy/middleware-serde': 3.0.6 + '@smithy/node-config-provider': 3.1.7 + '@smithy/shared-ini-file-loader': 3.1.7 + '@smithy/types': 3.4.2 + '@smithy/url-parser': 3.0.6 + '@smithy/util-middleware': 3.0.6 tslib: 2.6.2 - uuid: 8.3.2 optional: true - /@smithy/middleware-serde@2.0.11: - resolution: {integrity: sha512-NuxnjMyf4zQqhwwdh0OTj5RqpnuT6HcH5Xg5GrPijPcKzc2REXVEVK4Yyk8ckj8ez1XSj/bCmJ+oNjmqB02GWA==} - engines: {node: '>=14.0.0'} + /@smithy/middleware-retry@3.0.20: + resolution: {integrity: sha512-HELCOVwYw5hFDBm69d+LmmGjBCjWnwp/t7SJiHmp+c4u9vgfIaCjdSeIdnlOsLrr5ic5jGTJXvJFUQnd987b/g==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/node-config-provider': 3.1.7 + '@smithy/protocol-http': 4.1.3 + '@smithy/service-error-classification': 3.0.6 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 + '@smithy/util-middleware': 3.0.6 + '@smithy/util-retry': 3.0.6 tslib: 2.6.2 + uuid: 9.0.1 optional: true - /@smithy/middleware-stack@2.0.5: - resolution: {integrity: sha512-bVQU/rZzBY7CbSxIrDTGZYnBWKtIw+PL/cRc9B7etZk1IKSOe0NvKMJyWllfhfhrTeMF6eleCzOihIQympAvPw==} - engines: {node: '>=14.0.0'} + /@smithy/middleware-serde@3.0.6: + resolution: {integrity: sha512-KKTUSl1MzOM0MAjGbudeaVNtIDo+PpekTBkCNwvfZlKndodrnvRo+00USatiyLOc0ujjO9UydMRu3O9dYML7ag==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/node-config-provider@2.1.1: - resolution: {integrity: sha512-1lF6s1YWBi1LBu2O30tD3jyTgMtuvk/Z1twzXM4GPYe4dmZix4nNREPJIPOcfFikNU2o0eTYP80+izx5F2jIJA==} - engines: {node: '>=14.0.0'} + /@smithy/middleware-stack@3.0.6: + resolution: {integrity: sha512-2c0eSYhTQ8xQqHMcRxLMpadFbTXg6Zla5l0mwNftFCZMQmuhI7EbAJMx6R5eqfuV3YbJ3QGyS3d5uSmrHV8Khg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/property-provider': 2.0.12 - '@smithy/shared-ini-file-loader': 2.2.0 - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/node-http-handler@2.1.7: - resolution: {integrity: sha512-PQIKZXlp3awCDn/xNlCSTFE7aYG/5Tx33M05NfQmWYeB5yV1GZZOSz4dXpwiNJYTXb9jPqjl+ueXXkwtEluFFA==} - engines: {node: '>=14.0.0'} + /@smithy/node-config-provider@3.1.7: + resolution: {integrity: sha512-g3mfnC3Oo8pOI0dYuPXLtdW1WGVb3bR2tkV21GNkm0ZvQjLTtamXAwCWt/FCb0HGvKt3gHHmF1XerG0ICfalOg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/abort-controller': 2.0.11 - '@smithy/protocol-http': 3.0.7 - '@smithy/querystring-builder': 2.0.11 - '@smithy/types': 2.3.5 + '@smithy/property-provider': 3.1.6 + '@smithy/shared-ini-file-loader': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/property-provider@2.0.12: - resolution: {integrity: sha512-Un/OvvuQ1Kg8WYtoMCicfsFFuHb/TKL3pCA6ZIo/WvNTJTR94RtoRnL7mY4XkkUAoFMyf6KjcQJ76y1FX7S5rw==} - engines: {node: '>=14.0.0'} + /@smithy/node-http-handler@3.2.3: + resolution: {integrity: sha512-/gcm5DJ3k1b1zEInzBGAZC8ntJ+jwrz1NcSIu+9dSXd1FfG0G6QgkDI40tt8/WYUbHtLyo8fEqtm2v29koWo/w==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/abort-controller': 3.1.4 + '@smithy/protocol-http': 4.1.3 + '@smithy/querystring-builder': 3.0.6 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/protocol-http@3.0.7: - resolution: {integrity: sha512-HnZW8y+r66ntYueCDbLqKwWcMNWW8o3eVpSrHNluwtBJ/EUWfQHRKSiu6vZZtc6PGfPQWgVfucoCE/C3QufMAA==} - engines: {node: '>=14.0.0'} + /@smithy/property-provider@3.1.6: + resolution: {integrity: sha512-NK3y/T7Q/Bw+Z8vsVs9MYIQ5v7gOX7clyrXcwhhIBQhbPgRl6JDrZbusO9qWDhcEus75Tg+VCxtIRfo3H76fpw==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/querystring-builder@2.0.11: - resolution: {integrity: sha512-b4kEbVMxpmfv2VWUITn2otckTi7GlMteZQxi+jlwedoATOGEyrCJPfRcYQJjbCi3fZ2QTfh3PcORvB27+j38Yg==} - engines: {node: '>=14.0.0'} + /@smithy/protocol-http@4.1.3: + resolution: {integrity: sha512-GcbMmOYpH9iRqtC05RbRnc/0FssxSTHlmaNhYBTgSgNCYpdR3Kt88u5GAZTBmouzv+Zlj/VRv92J9ruuDeJuEw==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 - '@smithy/util-uri-escape': 2.0.0 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/querystring-parser@2.0.11: - resolution: {integrity: sha512-YXe7jhi7s3dQ0Fu9dLoY/gLu6NCyy8tBWJL/v2c9i7/RLpHgKT+uT96/OqZkHizCJ4kr0ZD46tzMjql/o60KLg==} - engines: {node: '>=14.0.0'} + /@smithy/querystring-builder@3.0.6: + resolution: {integrity: sha512-sQe08RunoObe+Usujn9+R2zrLuQERi3CWvRO3BvnoWSYUaIrLKuAIeY7cMeDax6xGyfIP3x/yFWbEKSXvOnvVg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 + '@smithy/util-uri-escape': 3.0.0 tslib: 2.6.2 optional: true - /@smithy/service-error-classification@2.0.4: - resolution: {integrity: sha512-77506l12I5gxTZqBkx3Wb0RqMG81bMYLaVQ+EqIWFwQDJRs5UFeXogKxSKojCmz1wLUziHZQXm03MBzPQiumQw==} - engines: {node: '>=14.0.0'} + /@smithy/querystring-parser@3.0.6: + resolution: {integrity: sha512-UJKw4LlEkytzz2Wq+uIdHf6qOtFfee/o7ruH0jF5I6UAuU+19r9QV7nU3P/uI0l6+oElRHmG/5cBBcGJrD7Ozg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 + tslib: 2.6.2 optional: true - /@smithy/shared-ini-file-loader@2.2.0: - resolution: {integrity: sha512-xFXqs4vAb5BdkzHSRrTapFoaqS4/3m/CGZzdw46fBjYZ0paYuLAoMY60ICCn1FfGirG+PiJ3eWcqJNe4/SkfyA==} - engines: {node: '>=14.0.0'} + /@smithy/service-error-classification@3.0.6: + resolution: {integrity: sha512-53SpchU3+DUZrN7J6sBx9tBiCVGzsib2e4sc512Q7K9fpC5zkJKs6Z9s+qbMxSYrkEkle6hnMtrts7XNkMJJMg==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + '@smithy/types': 3.4.2 + optional: true + + /@smithy/shared-ini-file-loader@3.1.7: + resolution: {integrity: sha512-IA4K2qTJYXkF5OfVN4vsY1hfnUZjaslEE8Fsr/gGFza4TAC2A9NfnZuSY2srQIbt9bwtjHiAayrRVgKse4Q7fA==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/signature-v4@2.0.11: - resolution: {integrity: sha512-EFVU1dT+2s8xi227l1A9O27edT/GNKvyAK6lZnIZ0zhIHq/jSLznvkk15aonGAM1kmhmZBVGpI7Tt0odueZK9A==} - engines: {node: '>=14.0.0'} + /@smithy/signature-v4@4.1.4: + resolution: {integrity: sha512-72MiK7xYukNsnLJI9NqvUHqTu0ziEsfMsYNlWpiJfuGQnCTFKpckThlEatirvcA/LmT1h7rRO+pJD06PYsPu9Q==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/eventstream-codec': 2.0.11 - '@smithy/is-array-buffer': 2.0.0 - '@smithy/types': 2.3.5 - '@smithy/util-hex-encoding': 2.0.0 - '@smithy/util-middleware': 2.0.4 - '@smithy/util-uri-escape': 2.0.0 - '@smithy/util-utf8': 2.0.0 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.3 + '@smithy/types': 3.4.2 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.6 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 optional: true - /@smithy/smithy-client@2.1.10: - resolution: {integrity: sha512-2OEmZDiW1Z196QHuQZ5M6cBE8FCSG0H2HADP1G+DY8P3agsvb0YJyfhyKuJbxIQy15tr3eDAK6FOrlbxgKOOew==} - engines: {node: '>=14.0.0'} + /@smithy/smithy-client@3.3.4: + resolution: {integrity: sha512-NKw/2XxOW/Rg3rzB90HxsmGok5oS6vRzJgMh/JN4BHaOQQ4q5OuX999GmOGxEp730wbpIXIowfKZmIMXkG4v0Q==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/middleware-stack': 2.0.5 - '@smithy/types': 2.3.5 - '@smithy/util-stream': 2.0.15 + '@smithy/middleware-endpoint': 3.1.3 + '@smithy/middleware-stack': 3.0.6 + '@smithy/protocol-http': 4.1.3 + '@smithy/types': 3.4.2 + '@smithy/util-stream': 3.1.8 tslib: 2.6.2 optional: true - /@smithy/types@2.3.5: - resolution: {integrity: sha512-ehyDt8M9hehyxrLQGoA1BGPou8Js1Ocoh5M0ngDhJMqbFmNK5N6Xhr9/ZExWkyIW8XcGkiMPq3ZUEE0ScrhbuQ==} - engines: {node: '>=14.0.0'} + /@smithy/types@3.4.2: + resolution: {integrity: sha512-tHiFcfcVedVBHpmHUEUHOCCih8iZbIAYn9NvPsNzaPm/237I3imdDdZoOC8c87H5HBAVEa06tTgb+OcSWV9g5w==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: tslib: 2.6.2 optional: true - /@smithy/url-parser@2.0.11: - resolution: {integrity: sha512-h89yXMCCF+S5k9XIoKltMIWTYj+FcEkU/IIFZ6RtE222fskOTL4Iak6ZRG+ehSvZDt8yKEcxqheTDq7JvvtK3g==} + /@smithy/url-parser@3.0.6: + resolution: {integrity: sha512-47Op/NU8Opt49KyGpHtVdnmmJMsp2hEwBdyjuFB9M2V5QVOwA7pBhhxKN5z6ztKGrMw76gd8MlbPuzzvaAncuQ==} requiresBuild: true dependencies: - '@smithy/querystring-parser': 2.0.11 - '@smithy/types': 2.3.5 + '@smithy/querystring-parser': 3.0.6 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/util-base64@2.0.0: - resolution: {integrity: sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==} - engines: {node: '>=14.0.0'} + /@smithy/util-base64@3.0.0: + resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 optional: true - /@smithy/util-body-length-browser@2.0.0: - resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==} + /@smithy/util-body-length-browser@3.0.0: + resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} requiresBuild: true dependencies: tslib: 2.6.2 optional: true - /@smithy/util-body-length-node@2.1.0: - resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==} - engines: {node: '>=14.0.0'} + /@smithy/util-body-length-node@3.0.0: + resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: tslib: 2.6.2 optional: true - /@smithy/util-buffer-from@2.0.0: - resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==} + /@smithy/util-buffer-from@2.2.0: + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} requiresBuild: true dependencies: - '@smithy/is-array-buffer': 2.0.0 + '@smithy/is-array-buffer': 2.2.0 tslib: 2.6.2 optional: true - /@smithy/util-config-provider@2.0.0: - resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} - engines: {node: '>=14.0.0'} + /@smithy/util-buffer-from@3.0.0: + resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: + '@smithy/is-array-buffer': 3.0.0 tslib: 2.6.2 optional: true - /@smithy/util-defaults-mode-browser@2.0.14: - resolution: {integrity: sha512-NupG7SWUucm3vJrvlpt9jG1XeoPJphjcivgcUUXhDJbUPy4F04LhlTiAhWSzwlCNcF8OJsMvZ/DWbpYD3pselw==} + /@smithy/util-config-provider@3.0.0: + resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + tslib: 2.6.2 + optional: true + + /@smithy/util-defaults-mode-browser@3.0.20: + resolution: {integrity: sha512-HpYmCpEThQJpCKzwzrGrklhdegRfuXI9keHRrHidbyEMliCdgic6t38MikJeZEkdIcEMhO1g95HIYMzjUzB+xg==} engines: {node: '>= 10.0.0'} requiresBuild: true dependencies: - '@smithy/property-provider': 2.0.12 - '@smithy/smithy-client': 2.1.10 - '@smithy/types': 2.3.5 + '@smithy/property-provider': 3.1.6 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 bowser: 2.11.0 tslib: 2.6.2 optional: true - /@smithy/util-defaults-mode-node@2.0.18: - resolution: {integrity: sha512-+3jMom/b/Cdp21tDnY4vKu249Al+G/P0HbRbct7/aSZDlROzv1tksaYukon6UUv7uoHn+/McqnsvqZHLlqvQ0g==} + /@smithy/util-defaults-mode-node@3.0.20: + resolution: {integrity: sha512-atdsHNtAX0rwTvRRGsrONU0C0XzapH6tI8T1y/OReOvWN7uBwXqqWRft6m8egU2DgeReU0xqT3PHdGCe5VRaaQ==} engines: {node: '>= 10.0.0'} requiresBuild: true dependencies: - '@smithy/config-resolver': 2.0.14 - '@smithy/credential-provider-imds': 2.0.16 - '@smithy/node-config-provider': 2.1.1 - '@smithy/property-provider': 2.0.12 - '@smithy/smithy-client': 2.1.10 - '@smithy/types': 2.3.5 + '@smithy/config-resolver': 3.0.8 + '@smithy/credential-provider-imds': 3.2.3 + '@smithy/node-config-provider': 3.1.7 + '@smithy/property-provider': 3.1.6 + '@smithy/smithy-client': 3.3.4 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/util-hex-encoding@2.0.0: - resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} - engines: {node: '>=14.0.0'} + /@smithy/util-endpoints@2.1.2: + resolution: {integrity: sha512-FEISzffb4H8DLzGq1g4MuDpcv6CIG15fXoQzDH9SjpRJv6h7J++1STFWWinilG0tQh9H1v2UKWG19Jjr2B16zQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: + '@smithy/node-config-provider': 3.1.7 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/util-middleware@2.0.4: - resolution: {integrity: sha512-Pbu6P4MBwRcjrLgdTR1O4Y3c0sTZn2JdOiJNcgL7EcIStcQodj+6ZTXtbyU/WTEU3MV2NMA10LxFc3AWHZ3+4A==} - engines: {node: '>=14.0.0'} + /@smithy/util-hex-encoding@3.0.0: + resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/types': 2.3.5 tslib: 2.6.2 optional: true - /@smithy/util-retry@2.0.4: - resolution: {integrity: sha512-b+n1jBBKc77C1E/zfBe1Zo7S9OXGBiGn55N0apfhZHxPUP/fMH5AhFUUcWaJh7NAnah284M5lGkBKuhnr3yK5w==} - engines: {node: '>= 14.0.0'} + /@smithy/util-middleware@3.0.6: + resolution: {integrity: sha512-BxbX4aBhI1O9p87/xM+zWy0GzT3CEVcXFPBRDoHAM+pV0eSW156pR+PSYEz0DQHDMYDsYAflC2bQNz2uaDBUZQ==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/service-error-classification': 2.0.4 - '@smithy/types': 2.3.5 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/util-stream@2.0.15: - resolution: {integrity: sha512-A/hkYJPH2N5MCWYvky4tTpQihpYAEzqnUfxDyG3L/yMndy/2sLvxnyQal9Opuj1e9FiKSTeMyjnU9xxZGs0mRw==} - engines: {node: '>=14.0.0'} + /@smithy/util-retry@3.0.6: + resolution: {integrity: sha512-BRZiuF7IwDntAbevqMco67an0Sr9oLQJqqRCsSPZZHYRnehS0LHDAkJk/pSmI7Z8c/1Vet294H7fY2fWUgB+Rg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: - '@smithy/fetch-http-handler': 2.2.2 - '@smithy/node-http-handler': 2.1.7 - '@smithy/types': 2.3.5 - '@smithy/util-base64': 2.0.0 - '@smithy/util-buffer-from': 2.0.0 - '@smithy/util-hex-encoding': 2.0.0 - '@smithy/util-utf8': 2.0.0 + '@smithy/service-error-classification': 3.0.6 + '@smithy/types': 3.4.2 tslib: 2.6.2 optional: true - /@smithy/util-uri-escape@2.0.0: - resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} - engines: {node: '>=14.0.0'} + /@smithy/util-stream@3.1.8: + resolution: {integrity: sha512-hoKOqSmb8FD3WLObuB5hwbM7bNIWgcnvkThokTvVq7J5PKjlLUK5qQQcB9zWLHIoSaIlf3VIv2OxZY2wtQjcRQ==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + '@smithy/fetch-http-handler': 3.2.8 + '@smithy/node-http-handler': 3.2.3 + '@smithy/types': 3.4.2 + '@smithy/util-base64': 3.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.2 + optional: true + + /@smithy/util-uri-escape@3.0.0: + resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} + engines: {node: '>=16.0.0'} requiresBuild: true dependencies: tslib: 2.6.2 optional: true - /@smithy/util-utf8@2.0.0: - resolution: {integrity: sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==} + /@smithy/util-utf8@2.3.0: + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} requiresBuild: true dependencies: - '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.6.2 + optional: true + + /@smithy/util-utf8@3.0.0: + resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + '@smithy/util-buffer-from': 3.0.0 tslib: 2.6.2 optional: true @@ -3634,13 +3698,13 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.4.7 + '@types/node': 22.6.1 dev: false /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.4.7 + '@types/node': 22.6.1 dev: false /@types/cookie@0.4.1: @@ -3660,7 +3724,7 @@ packages: /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: - '@types/node': 20.4.7 + '@types/node': 22.6.1 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 @@ -3680,7 +3744,7 @@ packages: requiresBuild: true dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.4.7 + '@types/node': 22.6.1 dev: false optional: true @@ -3698,11 +3762,11 @@ packages: /@types/jsonwebtoken@9.0.2: resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} dependencies: - '@types/node': 20.4.7 + '@types/node': 22.6.1 dev: false - /@types/linkify-it@3.0.2: - resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} + /@types/linkify-it@5.0.0: + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} requiresBuild: true dev: false optional: true @@ -3713,17 +3777,17 @@ packages: dev: false optional: true - /@types/markdown-it@12.2.3: - resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} + /@types/markdown-it@14.1.2: + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} requiresBuild: true dependencies: - '@types/linkify-it': 3.0.2 - '@types/mdurl': 1.0.2 + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 dev: false optional: true - /@types/mdurl@1.0.2: - resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} + /@types/mdurl@2.0.0: + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} requiresBuild: true dev: false optional: true @@ -3741,6 +3805,12 @@ packages: /@types/node@20.4.7: resolution: {integrity: sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==} + /@types/node@22.6.1: + resolution: {integrity: sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==} + dependencies: + undici-types: 6.19.8 + dev: false + /@types/object.omit@3.0.0: resolution: {integrity: sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==} dev: false @@ -3790,7 +3860,7 @@ packages: requiresBuild: true dependencies: '@types/glob': 8.1.0 - '@types/node': 20.4.7 + '@types/node': 22.6.1 dev: false optional: true @@ -3802,7 +3872,7 @@ packages: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 20.4.7 + '@types/node': 22.6.1 dev: false /@types/serve-static@1.15.2: @@ -3810,7 +3880,7 @@ packages: dependencies: '@types/http-errors': 2.0.1 '@types/mime': 1.3.2 - '@types/node': 20.4.7 + '@types/node': 22.6.1 dev: false /@types/throttle-debounce@2.1.0: @@ -4475,7 +4545,7 @@ packages: debug: 4.3.4 express-session: 1.17.3 kruptein: 3.0.6 - mongodb: 4.17.1 + mongodb: 4.17.1(@aws-sdk/client-sso-oidc@3.654.0) transitivePeerDependencies: - supports-color dev: false @@ -4767,14 +4837,14 @@ packages: engines: {node: '>=12'} dev: false - /duplexify@4.1.2: - resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==} + /duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} requiresBuild: true dependencies: end-of-stream: 1.4.4 inherits: 2.0.4 readable-stream: 3.6.2 - stream-shift: 1.0.1 + stream-shift: 1.0.3 dev: false optional: true @@ -4837,15 +4907,12 @@ packages: - utf-8-validate dev: false - /ent@2.2.0: - resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} - requiresBuild: true - dev: false - optional: true - - /entities@2.1.0: - resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + /ent@2.2.1: + resolution: {integrity: sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==} + engines: {node: '>= 0.4'} requiresBuild: true + dependencies: + punycode: 1.4.1 dev: false optional: true @@ -4854,6 +4921,13 @@ packages: engines: {node: '>=0.12'} dev: false + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + requiresBuild: true + dev: false + optional: true + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -5176,23 +5250,14 @@ packages: dev: false optional: true - /fast-xml-parser@4.2.5: - resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} + /fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true requiresBuild: true dependencies: strnum: 1.0.5 optional: true - /fast-xml-parser@4.2.7: - resolution: {integrity: sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==} - hasBin: true - requiresBuild: true - dependencies: - strnum: 1.0.5 - dev: false - optional: true - /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -5269,18 +5334,18 @@ packages: locate-path: 6.0.0 path-exists: 4.0.0 - /firebase-admin@11.10.1: - resolution: {integrity: sha512-atv1E6GbuvcvWaD3eHwrjeP5dAVs+EaHEJhu9CThMzPY6In8QYDiUR6tq5SwGl4SdA/GcAU0nhwWc/FSJsAzfQ==} + /firebase-admin@11.11.1: + resolution: {integrity: sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==} engines: {node: '>=14'} dependencies: '@fastify/busboy': 1.2.1 '@firebase/database-compat': 0.3.4 '@firebase/database-types': 0.10.4 - '@types/node': 20.4.7 + '@types/node': 22.6.1 jsonwebtoken: 9.0.2 - jwks-rsa: 3.0.1 + jwks-rsa: 3.1.0 node-forge: 1.3.1 - uuid: 9.0.0 + uuid: 9.0.1 optionalDependencies: '@google-cloud/firestore': 6.8.0 '@google-cloud/storage': 6.12.0 @@ -5379,7 +5444,7 @@ packages: extend: 3.0.2 https-proxy-agent: 5.0.1 is-stream: 2.0.1 - node-fetch: 2.6.12 + node-fetch: 2.7.0 transitivePeerDependencies: - encoding - supports-color @@ -5461,12 +5526,13 @@ packages: /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported requiresBuild: true dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 5.1.6 + minimatch: 5.0.1 once: 1.4.0 dev: false optional: true @@ -5519,16 +5585,16 @@ packages: hasBin: true requiresBuild: true dependencies: - '@grpc/grpc-js': 1.8.21 - '@grpc/proto-loader': 0.7.8 + '@grpc/grpc-js': 1.8.22 + '@grpc/proto-loader': 0.7.13 '@types/long': 4.0.2 '@types/rimraf': 3.0.2 abort-controller: 3.0.0 - duplexify: 4.1.2 + duplexify: 4.1.3 fast-text-encoding: 1.0.6 google-auth-library: 8.9.0 is-stream-ended: 0.1.4 - node-fetch: 2.6.12 + node-fetch: 2.7.0 object-hash: 3.0.0 proto3-json-serializer: 1.1.1 protobufjs: 7.2.4 @@ -5543,6 +5609,7 @@ packages: /google-p12-pem@4.0.1: resolution: {integrity: sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==} engines: {node: '>=12.0.0'} + deprecated: Package is no longer maintained hasBin: true requiresBuild: true dependencies: @@ -5874,8 +5941,8 @@ packages: engines: {node: '>= 0.6.0'} dev: false - /jose@4.14.4: - resolution: {integrity: sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==} + /jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} dev: false /js-tokens@4.0.0: @@ -5896,27 +5963,27 @@ packages: dev: false optional: true - /jsdoc@4.0.2: - resolution: {integrity: sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==} + /jsdoc@4.0.3: + resolution: {integrity: sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==} engines: {node: '>=12.0.0'} hasBin: true requiresBuild: true dependencies: '@babel/parser': 7.22.7 - '@jsdoc/salty': 0.2.5 - '@types/markdown-it': 12.2.3 + '@jsdoc/salty': 0.2.8 + '@types/markdown-it': 14.1.2 bluebird: 3.7.2 catharsis: 0.9.0 escape-string-regexp: 2.0.0 js2xmlparser: 4.0.2 klaw: 3.0.0 - markdown-it: 12.3.2 - markdown-it-anchor: 8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2) + markdown-it: 14.1.0 + markdown-it-anchor: 8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0) marked: 4.3.0 mkdirp: 1.0.4 requizzle: 0.2.4 strip-json-comments: 3.1.1 - underscore: 1.13.6 + underscore: 1.13.7 dev: false optional: true @@ -5993,14 +6060,14 @@ packages: dev: false optional: true - /jwks-rsa@3.0.1: - resolution: {integrity: sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==} + /jwks-rsa@3.1.0: + resolution: {integrity: sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==} engines: {node: '>=14'} dependencies: '@types/express': 4.17.21 '@types/jsonwebtoken': 9.0.2 debug: 4.3.4 - jose: 4.14.4 + jose: 4.15.9 limiter: 1.1.5 lru-memoizer: 2.2.0 transitivePeerDependencies: @@ -6077,19 +6144,19 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: false - /linkify-it@3.0.3: - resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} - requiresBuild: true + /linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} dependencies: uc.micro: 1.0.6 dev: false - optional: true - /linkify-it@4.0.1: - resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + requiresBuild: true dependencies: - uc.micro: 1.0.6 + uc.micro: 2.1.0 dev: false + optional: true /linkifyjs@3.0.5: resolution: {integrity: sha512-1Y9XQH65eQKA9p2xtk+zxvnTeQBG7rdAXSkUG97DmuI/Xhji9uaUzaWxRj6rf9YC0v8KKHkxav7tnLX82Sz5Fg==} @@ -6188,12 +6255,6 @@ packages: triple-beam: 1.4.1 dev: false - /long@4.0.0: - resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} - requiresBuild: true - dev: false - optional: true - /long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} requiresBuild: true @@ -6257,41 +6318,42 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: false - /markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2): + /markdown-it-anchor@8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0): resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} requiresBuild: true peerDependencies: '@types/markdown-it': '*' markdown-it: '*' dependencies: - '@types/markdown-it': 12.2.3 - markdown-it: 12.3.2 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 dev: false optional: true - /markdown-it@12.3.2: - resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + /markdown-it@13.0.1: + resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} hasBin: true - requiresBuild: true dependencies: argparse: 2.0.1 - entities: 2.1.0 - linkify-it: 3.0.3 + entities: 3.0.1 + linkify-it: 4.0.1 mdurl: 1.0.1 uc.micro: 1.0.6 dev: false - optional: true - /markdown-it@13.0.1: - resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} + /markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + requiresBuild: true dependencies: argparse: 2.0.1 - entities: 3.0.1 - linkify-it: 4.0.1 - mdurl: 1.0.1 - uc.micro: 1.0.6 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 dev: false + optional: true /marked@4.3.0: resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} @@ -6305,6 +6367,12 @@ packages: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} dev: false + /mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + requiresBuild: true + dev: false + optional: true + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -6391,16 +6459,6 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 - dev: true - - /minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - requiresBuild: true - dependencies: - brace-expansion: 2.0.1 - dev: false - optional: true /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -6454,7 +6512,7 @@ packages: '@types/whatwg-url': 8.2.2 whatwg-url: 11.0.0 - /mongodb@4.17.1: + /mongodb@4.17.1(@aws-sdk/client-sso-oidc@3.654.0): resolution: {integrity: sha512-MBuyYiPUPRTqfH2dV0ya4dcr2E5N52ocBuZ8Sgg/M030nGF78v855B3Z27mZJnp8PxjnUquEnAtjOsphgMZOlQ==} engines: {node: '>=12.9.0'} dependencies: @@ -6462,23 +6520,25 @@ packages: mongodb-connection-string-url: 2.6.0 socks: 2.7.1 optionalDependencies: - '@aws-sdk/credential-providers': 3.427.0 - '@mongodb-js/saslprep': 1.1.0 + '@aws-sdk/credential-providers': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0) + '@mongodb-js/saslprep': 1.1.9 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt - /mongoose@6.12.0: + /mongoose@6.12.0(@aws-sdk/client-sso-oidc@3.654.0): resolution: {integrity: sha512-sd/q83C6TBRPBrrD2A/POSbA/exbCFM2WOuY7Lf2JuIJFlHFG39zYSDTTAEiYlzIfahNOLmXPxBGFxdAch41Mw==} engines: {node: '>=12.0.0'} dependencies: bson: 4.7.2 kareem: 2.5.1 - mongodb: 4.17.1 + mongodb: 4.17.1(@aws-sdk/client-sso-oidc@3.654.0) mpath: 0.9.0 mquery: 4.0.3 ms: 2.1.3 sift: 16.0.1 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt - supports-color dev: false @@ -6528,8 +6588,8 @@ packages: uuid: 8.3.2 dev: false - /node-fetch@2.6.12: - resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} requiresBuild: true peerDependencies: @@ -7001,7 +7061,7 @@ packages: engines: {node: '>=12.0.0'} requiresBuild: true dependencies: - protobufjs: 7.2.5 + protobufjs: 7.4.0 dev: false optional: true @@ -7018,12 +7078,12 @@ packages: espree: 9.6.1 estraverse: 5.3.0 glob: 8.1.0 - jsdoc: 4.0.2 + jsdoc: 4.0.3 minimist: 1.2.8 protobufjs: 7.2.4 semver: 7.5.4 - tmp: 0.2.1 - uglify-js: 3.17.4 + tmp: 0.2.3 + uglify-js: 3.19.3 dev: false optional: true @@ -7042,13 +7102,13 @@ packages: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.4.7 + '@types/node': 22.6.1 long: 5.2.3 dev: false optional: true - /protobufjs@7.2.5: - resolution: {integrity: sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==} + /protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} engines: {node: '>=12.0.0'} requiresBuild: true dependencies: @@ -7062,7 +7122,7 @@ packages: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.4.7 + '@types/node': 22.6.1 long: 5.2.3 dev: false optional: true @@ -7083,10 +7143,23 @@ packages: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} dev: true + /punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + optional: true + /punycode@1.3.2: resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} dev: false + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + requiresBuild: true + dev: false + optional: true + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -7769,8 +7842,8 @@ packages: dev: false optional: true - /stream-shift@1.0.1: - resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} + /stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} requiresBuild: true dev: false optional: true @@ -7928,9 +8001,9 @@ packages: dependencies: http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 - node-fetch: 2.6.12 + node-fetch: 2.7.0 stream-events: 1.0.5 - uuid: 9.0.0 + uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color @@ -7978,12 +8051,10 @@ packages: '@popperjs/core': 2.11.8 dev: false - /tmp@0.2.1: - resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} - engines: {node: '>=8.17.0'} + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} requiresBuild: true - dependencies: - rimraf: 3.0.2 dev: false optional: true @@ -8027,13 +8098,14 @@ packages: engines: {node: '>= 14.0.0'} dev: false - /tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} requiresBuild: true optional: true - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + dev: false /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} @@ -8076,8 +8148,14 @@ packages: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} dev: false - /uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + /uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + requiresBuild: true + dev: false + optional: true + + /uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true @@ -8095,12 +8173,16 @@ packages: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} dev: true - /underscore@1.13.6: - resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} + /underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} requiresBuild: true dev: false optional: true + /undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + dev: false + /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -8208,11 +8290,12 @@ packages: /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + dev: false - /uuid@9.0.0: - resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - dev: false + requiresBuild: true /v8-compile-cache@2.3.0: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} diff --git a/src/modules/fcm.js b/src/modules/fcm.js index 1fc53946..25ca9e72 100644 --- a/src/modules/fcm.js +++ b/src/modules/fcm.js @@ -196,7 +196,7 @@ const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { title, body, url: link || "/", - icon: icon || "/icons-512.png", + icon: icon || "https://taxi.sparcs.org/icons-512.png", click_action: "FLUTTER_NOTIFICATION_CLICK", }, apns: { payload: { aps: { alert: { title, body } } } }, @@ -204,9 +204,8 @@ const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { ttl: 0, }, }; - const { responses, failureCount } = await getMessaging().sendMulticast( - message - ); + const { responses, failureCount } = + await getMessaging().sendEachForMulticast(message); // 메시지 전송에 실패한 기기가 존재할 경우, 해당 기기의 deviceToken을 DB에서 삭제합니다. if (failureCount) { From e8d26d61124441d8caab5d7e86c0efdfdd233cc6 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 25 Sep 2024 01:42:20 +0900 Subject: [PATCH 115/151] Refactor: use high priority fcm for android devices --- src/modules/fcm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/fcm.js b/src/modules/fcm.js index 25ca9e72..fa37f64e 100644 --- a/src/modules/fcm.js +++ b/src/modules/fcm.js @@ -201,7 +201,7 @@ const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { }, apns: { payload: { aps: { alert: { title, body } } } }, android: { - ttl: 0, + priority: "high", }, }; const { responses, failureCount } = From 87eaf2ec95584708a96b6aacb67568e4976a94c6 Mon Sep 17 00:00:00 2001 From: neymar <0208mjkim@gmail.com> Date: Tue, 5 Nov 2024 23:43:46 +0900 Subject: [PATCH 116/151] Refactor: migrate routes/users and services/users to ts --- src/routes/{users.js => users.ts} | 12 ++++---- src/services/{users.js => users.ts} | 43 +++++++++++++---------------- 2 files changed, 25 insertions(+), 30 deletions(-) rename src/routes/{users.js => users.ts} (82%) rename src/services/{users.js => users.ts} (82%) diff --git a/src/routes/users.js b/src/routes/users.ts similarity index 82% rename from src/routes/users.js rename to src/routes/users.ts index aa00bcb2..b3020054 100755 --- a/src/routes/users.js +++ b/src/routes/users.ts @@ -1,12 +1,12 @@ -const express = require("express"); -const { body } = require("express-validator"); -const validator = require("@/middlewares/validator").default; -const patterns = require("@/modules/patterns").default; +import express from "express"; +import { body } from "express-validator"; +import validator from "@/middlewares/validator"; +import patterns from "@/modules/patterns"; const router = express.Router(); -const userHandlers = require("@/services/users"); +import * as userHandlers from "@/services/users"; -const { replaceSpaceInNickname } = require("@/modules/modifyProfile"); +import { replaceSpaceInNickname } from "@/modules/modifyProfile"; // 라우터 접근 시 로그인 필요 router.use(require("@/middlewares/auth").default); diff --git a/src/services/users.js b/src/services/users.ts similarity index 82% rename from src/services/users.js rename to src/services/users.ts index 7197f01f..de83c073 100644 --- a/src/services/users.js +++ b/src/services/users.ts @@ -1,6 +1,9 @@ -const { userModel } = require("@/modules/stores/mongo"); -const logger = require("@/modules/logger").default; -const aws = require("@/modules/stores/aws"); +import type { Request, Response } from "express"; + +import { userModel } from "@/modules/stores/mongo"; +import logger from "@/modules/logger"; +import * as aws from "@/modules/stores/aws"; + const { generateNickname, generateProfileImageUrl, @@ -9,10 +12,10 @@ const { // 이벤트 코드입니다. // const { contracts } = require("@/lottery"); -const agreeOnTermsOfServiceHandler = async (req, res) => { +export const agreeOnTermsOfServiceHandler = async (req: Request, res: Response) => { try { let user = await userModel.findOne({ id: req.userId }); - if (user.agreeOnTermsOfService !== true) { + if (user && user.agreeOnTermsOfService !== true) { user.agreeOnTermsOfService = true; await user.save(); res @@ -28,13 +31,15 @@ const agreeOnTermsOfServiceHandler = async (req, res) => { } }; -const getAgreeOnTermsOfServiceHandler = async (req, res) => { +export const getAgreeOnTermsOfServiceHandler = async (req: Request, res: Response) => { try { const user = await userModel .findOne({ id: req.userId }, "agreeOnTermsOfService") .lean(); - const agreeOnTermsOfService = user.agreeOnTermsOfService === true; - res.json({ agreeOnTermsOfService }); + if (user) { + const agreeOnTermsOfService = user.agreeOnTermsOfService === true; + res.json({ agreeOnTermsOfService }); + } } catch { res .status(500) @@ -42,7 +47,7 @@ const getAgreeOnTermsOfServiceHandler = async (req, res) => { } }; -const editNicknameHandler = async (req, res) => { +export const editNicknameHandler = async (req: Request, res: Response) => { try { const newNickname = req.body.nickname; const result = await userModel.findOneAndUpdate( @@ -69,7 +74,7 @@ const editNicknameHandler = async (req, res) => { } }; -const editAccountHandler = async (req, res) => { +export const editAccountHandler = async (req: Request, res: Response) => { try { const newAccount = req.body.account; const result = await userModel.findOneAndUpdate( @@ -95,7 +100,7 @@ const editAccountHandler = async (req, res) => { } }; -const editProfileImgGetPUrlHandler = async (req, res) => { +export const editProfileImgGetPUrlHandler = async (req: Request, res: Response) => { try { const type = req.body.type; const user = await userModel.findOne({ id: req.userId }, "_id"); @@ -125,7 +130,7 @@ const editProfileImgGetPUrlHandler = async (req, res) => { } }; -const editProfileImgDoneHandler = async (req, res) => { +export const editProfileImgDoneHandler = async (req: Request, res: Response) => { try { const user = await userModel.findOne({ id: req.userId }, "_id"); if (!user) { @@ -161,7 +166,7 @@ const editProfileImgDoneHandler = async (req, res) => { } }; -const resetNicknameHandler = async (req, res) => { +export const resetNicknameHandler = async (req: Request, res: Response) => { try { const result = await userModel.findOneAndUpdate( { id: req.userId }, @@ -181,7 +186,7 @@ const resetNicknameHandler = async (req, res) => { } }; -const resetProfileImgHandler = async (req, res) => { +export const resetProfileImgHandler = async (req: Request, res: Response) => { try { const result = await userModel.findOneAndUpdate( { id: req.userId }, @@ -200,13 +205,3 @@ const resetProfileImgHandler = async (req, res) => { } }; -module.exports = { - agreeOnTermsOfServiceHandler, - getAgreeOnTermsOfServiceHandler, - editNicknameHandler, - editAccountHandler, - editProfileImgGetPUrlHandler, - editProfileImgDoneHandler, - resetNicknameHandler, - resetProfileImgHandler, -}; From a6db7d918800eaa814956eda4b96a96feb60b013 Mon Sep 17 00:00:00 2001 From: neymar <0208mjkim@gmail.com> Date: Tue, 5 Nov 2024 23:48:20 +0900 Subject: [PATCH 117/151] Fix: Updated one import in users --- src/services/users.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/services/users.ts b/src/services/users.ts index de83c073..3554690f 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -4,11 +4,8 @@ import { userModel } from "@/modules/stores/mongo"; import logger from "@/modules/logger"; import * as aws from "@/modules/stores/aws"; -const { - generateNickname, - generateProfileImageUrl, -} = require("@/modules/modifyProfile"); +import { generateNickname, generateProfileImageUrl } from "@/modules/modifyProfile"; // 이벤트 코드입니다. // const { contracts } = require("@/lottery"); From 226435bd360ccb239f52805511e47c973a8f7a8f Mon Sep 17 00:00:00 2001 From: static Date: Tue, 12 Nov 2024 22:57:53 +0900 Subject: [PATCH 118/151] Fix: merge conflicts --- src/index.ts | 6 +++++ src/loadenv.ts | 4 ++++ .../routes/docs/schemas/globalStateSchema.js | 2 +- .../routes/docs/schemas/invitesSchema.js | 2 +- .../routes/docs/schemas/itemsSchema.js | 2 +- src/modules/fare.js | 4 ++-- src/modules/fcm.ts | 2 +- src/modules/stores/mongo.ts | 12 +++++++--- src/routes/chats.js | 1 - src/routes/docs/chats.js | 2 +- src/routes/docs/logininfo.js | 2 +- src/routes/docs/reports.js | 2 +- src/routes/docs/rooms.js | 2 +- src/routes/docs/schemas/chatsSchema.js | 2 +- src/routes/docs/schemas/fareSchema.js | 2 +- src/routes/docs/users.js | 2 +- src/routes/index.ts | 1 + src/routes/users.ts | 2 +- src/schedules/index.ts | 7 ++++++ src/services/auth.js | 2 +- src/services/fare.js | 8 +++---- src/services/users.ts | 2 +- src/types/mongo.d.ts | 24 +++++++++++++++++++ 23 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index a3470242..f741d146 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,8 +24,10 @@ import { notificationRouter, adminRouter, docsRouter, + fareRouter, } from "@/routes"; import { initializeApp } from "@/modules/fcm"; +import { initializeDatabase as initializeFareDatabase } from "@/modules/fare"; import logger from "@/modules/logger"; import { connectDatabase } from "@/modules/stores/mongo"; import { startSocketServer } from "@/modules/socket"; @@ -85,6 +87,7 @@ app.use("/chats", chatRouter); app.use("/locations", locationRouter); app.use("/reports", reportRouter); app.use("/notifications", notificationRouter); +app.use("/fare", fareRouter); // [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. app.use(errorHandler); @@ -101,3 +104,6 @@ app.set("io", startSocketServer(serverHttp)); // [Schedule] 스케줄러 시작 registerSchedules(app); + +// [Module] 택시 예상 비용 db 초기화 +initializeFareDatabase(); diff --git a/src/loadenv.ts b/src/loadenv.ts index e251eab4..c70bb4fa 100644 --- a/src/loadenv.ts +++ b/src/loadenv.ts @@ -63,3 +63,7 @@ export const slackWebhookUrl = { }; // export const eventConfig = // process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG); // optional +export const naverMap = { + apiId: process.env.NAVER_MAP_API_ID || "", // optional + apiKey: process.env.NAVER_MAP_API_KEY || "", // optional +}; diff --git a/src/lottery/routes/docs/schemas/globalStateSchema.js b/src/lottery/routes/docs/schemas/globalStateSchema.js index 15055525..26fe4ed9 100644 --- a/src/lottery/routes/docs/schemas/globalStateSchema.js +++ b/src/lottery/routes/docs/schemas/globalStateSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); -const { objectId, user } = require("../../../../modules/patterns"); +const { objectId, user } = require("../../../../modules/patterns").default; const globalStateZod = { createUserGlobalStateHandler: z diff --git a/src/lottery/routes/docs/schemas/invitesSchema.js b/src/lottery/routes/docs/schemas/invitesSchema.js index dfc33c5c..d43498cc 100644 --- a/src/lottery/routes/docs/schemas/invitesSchema.js +++ b/src/lottery/routes/docs/schemas/invitesSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); -const { objectId } = require("../../../../modules/patterns"); +const { objectId } = require("../../../../modules/patterns").default; const invitesZod = { searchInviterHandler: z.object({ diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index d224ba70..68061d35 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); -const { objectId } = require("../../../../modules/patterns"); +const { objectId } = require("../../../../modules/patterns").default; const itemsZod = { getItemHandler: z.object({ diff --git a/src/modules/fare.js b/src/modules/fare.js index 350b8207..be3c3c4d 100644 --- a/src/modules/fare.js +++ b/src/modules/fare.js @@ -1,7 +1,7 @@ const axios = require("axios"); -const logger = require("./logger"); +const logger = require("./logger").default; -const { naverMap } = require("../../loadenv"); +const { naverMap } = require("@/loadenv"); const { taxiFareModel, locationModel } = require("./stores/mongo"); const naverMapApi = { diff --git a/src/modules/fcm.ts b/src/modules/fcm.ts index aac7ab9f..ae5abf5b 100644 --- a/src/modules/fcm.ts +++ b/src/modules/fcm.ts @@ -216,7 +216,7 @@ export const sendMessageByTokens = async ( }, apns: { payload: { aps: { alert: { title, body } } } }, android: { - priority: "high", + priority: "high" as const, }, }; const { responses, failureCount } = diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index 9549dc5d..b4affe32 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -2,6 +2,7 @@ import mongoose, { model, Schema, Types } from "mongoose"; import logger from "@/modules/logger"; import type { User, + Ban, Participant, DeviceToken, NotificationOption, @@ -12,6 +13,7 @@ import type { Report, AdminIPWhitelist, AdminLog, + TaxiFare, } from "@/types/mongo"; const userSchema = new Schema({ @@ -39,7 +41,7 @@ const userSchema = new Schema({ export const userModel = model("User", userSchema); -const banSchema = Schema({ +const banSchema = new Schema({ // 정지 시킬 사용자를 기제함. userSid: { type: String, required: true }, // 정지 사유 @@ -58,6 +60,8 @@ const banSchema = Schema({ }, }); +export const banModel = model("Ban", banSchema); + const participantSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, settlementStatus: { @@ -231,19 +235,21 @@ const adminLogSchema = new Schema({ export const adminLogModel = model("AdminLog", adminLogSchema); -const taxiFareSchema = Schema( +const taxiFareSchema = new Schema( { from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 출발지 to: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 도착지 isMajor: { type: Boolean, default: false }, // 카이스트 본원 <-> 대전역 경로 여부 time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) - fare: { type: Number, default: false }, // 예상 택시 요금 + fare: { type: Number, default: 0 }, // 예상 택시 요금 }, { timestamps: true, // 최근 업데이트 시간 기록용 } ); +export const taxiFareModel = model("TaxiFare", taxiFareSchema); + mongoose.set("strictQuery", true); const database = mongoose.connection; diff --git a/src/routes/chats.js b/src/routes/chats.js index d6f7ddce..32b01329 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -4,7 +4,6 @@ const validator = require("@/middlewares/validator").default; const patterns = require("@/modules/patterns").default; const { validateBody } = require("@/middlewares/zod"); const { chatsZod } = require("./docs/schemas/chatsSchema"); -const patterns = require("@/modules/patterns"); const router = express.Router(); const chatsHandlers = require("@/services/chats"); diff --git a/src/routes/docs/chats.js b/src/routes/docs/chats.js index 3d934181..3f4b63a3 100644 --- a/src/routes/docs/chats.js +++ b/src/routes/docs/chats.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns").default; +const { objectId } = require("@/modules/patterns").default; const tag = "chats"; const apiPrefix = "/chats"; diff --git a/src/routes/docs/logininfo.js b/src/routes/docs/logininfo.js index caec1f96..3f9c0b0b 100644 --- a/src/routes/docs/logininfo.js +++ b/src/routes/docs/logininfo.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns").default; +const { objectId } = require("@/modules/patterns").default; const tag = "logininfo"; const apiPrefix = "/logininfo"; diff --git a/src/routes/docs/reports.js b/src/routes/docs/reports.js index 3acf99da..bfc29687 100644 --- a/src/routes/docs/reports.js +++ b/src/routes/docs/reports.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns").default; +const { objectId } = require("@/modules/patterns").default; const tag = "reports"; const apiPrefix = "/reports"; diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index e8f3aa99..41d6a1cb 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -1,4 +1,4 @@ -const { objectId, room } = require("../../modules/patterns").default; +const { objectId, room } = require("@/modules/patterns").default; const tag = "rooms"; const apiPrefix = "/rooms"; diff --git a/src/routes/docs/schemas/chatsSchema.js b/src/routes/docs/schemas/chatsSchema.js index fa0ae257..5f1d534c 100644 --- a/src/routes/docs/schemas/chatsSchema.js +++ b/src/routes/docs/schemas/chatsSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../utils"); -const { objectId, chat } = require("../../../modules/patterns"); +const { objectId, chat } = require("@/modules/patterns").default; const chatsZod = { sendChatHandler: z.object({ diff --git a/src/routes/docs/schemas/fareSchema.js b/src/routes/docs/schemas/fareSchema.js index 0812a86a..5caa87e7 100644 --- a/src/routes/docs/schemas/fareSchema.js +++ b/src/routes/docs/schemas/fareSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../utils"); -const { objectId } = require("../../../modules/patterns"); +const { objectId } = require("../../../modules/patterns").default; const fareZod = { getTaxiFareHandler: z.object({ diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index 0f19ecde..f21c037a 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -1,6 +1,6 @@ const tag = "users"; const apiPrefix = "/users"; -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("../../modules/patterns").default; const usersDocs = {}; usersDocs[`${apiPrefix}/agreeOnTermsOfService`] = { diff --git a/src/routes/index.ts b/src/routes/index.ts index d2084458..ddbe083a 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -8,3 +8,4 @@ export { default as notificationRouter } from "./notifications"; export { default as reportRouter } from "./reports"; export { default as roomRouter } from "./rooms"; export { default as userRouter } from "./users"; +export { default as fareRouter } from "./fare"; diff --git a/src/routes/users.ts b/src/routes/users.ts index 9125adee..1f6cb288 100755 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -59,4 +59,4 @@ router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); // 유저의 서비스 정지 기록들을 모두 반환합니다. router.get("/getBanRecord", userHandlers.getBanRecordHandler); -module.exports = router; +export default router; diff --git a/src/schedules/index.ts b/src/schedules/index.ts index f9e62477..1908fadd 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -2,10 +2,17 @@ import { type Express } from "express"; import cron from "node-cron"; import notifyBeforeDepart from "./notifyBeforeDepart"; import notifyAfterArrival from "./notifyAfterArrival"; +import updateMajorTaxiFare from "./updateMajorTaxiFare"; +import updateMinorTaxiFare from "./updateMinorTaxiFare"; +import { naverMap } from "@/loadenv"; const registerSchedules = (app: Express) => { cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); cron.schedule("*/10 * * * *", notifyAfterArrival(app)); + if (naverMap.apiId && naverMap.apiKey) { + cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app)); + cron.schedule("0 18 * * *", updateMinorTaxiFare(app)); + } }; export default registerSchedules; diff --git a/src/services/auth.js b/src/services/auth.js index 6aa0932e..cd660ab5 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -1,6 +1,6 @@ const { sparcssso: sparcsssoEnv, nodeEnv, testAccounts } = require("@/loadenv"); const { userModel } = require("@/modules/stores/mongo"); -const { user: userPattern } = require("@/modules/patterns"); +const { user: userPattern } = require("@/modules/patterns").default; const { getLoginInfo, logout, login } = require("@/modules/auths/login"); const { unregisterDeviceToken } = require("@/modules/fcm"); diff --git a/src/services/fare.js b/src/services/fare.js index 638faa65..c8b41831 100644 --- a/src/services/fare.js +++ b/src/services/fare.js @@ -1,8 +1,8 @@ -const logger = require("../modules/logger"); +const logger = require("@/modules/logger").default; -const { naverMap } = require("../../loadenv"); -const { taxiFareModel, locationModel } = require("../modules/stores/mongo"); -const { scaledTime, callTaxiFare } = require("../modules/fare"); +const { naverMap } = require("@/loadenv"); +const { taxiFareModel, locationModel } = require("@/modules/stores/mongo"); +const { scaledTime, callTaxiFare } = require("@/modules/fare"); const naverMapApi = { "X-NCP-APIGW-API-KEY-ID": naverMap.apiId, diff --git a/src/services/users.ts b/src/services/users.ts index 3d3cfa11..2fc8f0ba 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -220,7 +220,7 @@ export const getBanRecordHandler = async (req: Request, res: Response) => { // 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 const result = await banModel .find({ - userSid: req.session.loginInfo.sid, + userSid: req.session.loginInfo?.sid, }) .sort({ expireAt: -1 }); if (!result) diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index 1f0bf64f..93fcb382 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -37,6 +37,19 @@ export interface User extends Document { account: string; } +export interface Ban extends Document { + /** 정지된 사용자의 ID. */ + userSid: string; + /** 정지 사유. */ + reason: string; + /** 정지 시각. */ + bannedAt: Date; + /** 정지 만료 시각. */ + expireAt: Date; + /** 정지된 서비스의 이름. */ + serviceName: "service" | "2023-fall-event"; +} + export type SettlementStatus = | "not-departed" | "paid" @@ -166,3 +179,14 @@ export interface AdminLog extends Document { /** 수행한 업무. */ action: AdminLogAction; } + +export interface TaxiFare extends Document { + /** 출발지의 Location ObjectID. */ + from: Types.ObjectId; + /** 목적지의 Location ObjectID. */ + to: Types.ObjectId; + isMajor: boolean; + time: number; + /** 예상 택시 요금. */ + fare: number; +} From 1c8dac4ec8b11c7882291a26556dfc9997efd015 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 12 Nov 2024 23:06:05 +0900 Subject: [PATCH 119/151] Refactor: enable CI --- .github/workflows/test_ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index 1f19bb94..2ccc5415 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -15,8 +15,6 @@ jobs: node-version: ['18.x'] mongodb-version: ['5.0'] steps: - - name: Exit because unit tests are not implemented in TypeScript for now - run: exit 1 - name: Start MongoDB run: sudo docker run --name mongodb -d -p 27017:27017 mongo:${{ matrix.mongodb-version }} - uses: actions/checkout@v3 From 7e17d5fa8bd9a1148d809c8feb24f2b2c28e0805 Mon Sep 17 00:00:00 2001 From: cokia Date: Tue, 19 Nov 2024 22:34:24 +0900 Subject: [PATCH 120/151] =?UTF-8?q?Fix:=20loadenv=20=EB=A5=BC=20As-Is?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8D=98=20Nes?= =?UTF-8?q?ted=20object=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EC=98=80?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/profileImageUrlUpdater.js | 2 +- src/index.ts | 10 +-- src/loadenv.ts | 98 ++++++++++++++++-------------- src/middlewares/cors.ts | 4 +- src/middlewares/session.ts | 8 +-- src/modules/auths/jwt.ts | 4 +- src/modules/auths/login.ts | 4 +- src/modules/fcm.ts | 8 ++- src/modules/logger.ts | 4 +- src/modules/slackNotification.ts | 11 ++-- src/modules/stores/aws.ts | 14 ++--- src/modules/stores/sessionStore.ts | 14 ++--- src/sampleGenerator/index.js | 2 +- src/schedules/index.ts | 4 +- 14 files changed, 95 insertions(+), 92 deletions(-) diff --git a/scripts/profileImageUrlUpdater.js b/scripts/profileImageUrlUpdater.js index 2b35bf1a..fdfa671e 100644 --- a/scripts/profileImageUrlUpdater.js +++ b/scripts/profileImageUrlUpdater.js @@ -2,7 +2,7 @@ // https://github.com/sparcs-kaist/taxi-back/issues/173 const { MongoClient } = require("mongodb"); -const { mongo: mongoUrl, aws: awsEnv } = require("../loadenv"); // FIXME: 올바른 경로로 수정해야 합니다. +const { mongoUrl, aws: awsEnv } = require("../loadenv"); // FIXME: 올바른 경로로 수정해야 합니다. const time = Date.now(); diff --git a/src/index.ts b/src/index.ts index f741d146..4aebcc46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import express from "express"; import cookieParser from "cookie-parser"; import http from "http"; -import { nodeEnv, mongo as mongoUrl, port as httpPort } from "@/loadenv"; +import config from "@/loadenv"; import { corsMiddleware, sessionMiddleware, @@ -40,14 +40,14 @@ initializeApp(); const app = express(); // 데이터베이스 연결 -connectDatabase(mongoUrl); +connectDatabase(config.mongoUrl); // [Middleware] request body 파싱 app.use(express.urlencoded({ extended: false })); app.use(express.json()); // reverse proxy가 설정한 헤더를 신뢰합니다. -if (nodeEnv === "production") app.set("trust proxy", 2); +if (config.nodeEnv === "production") app.set("trust proxy", 2); // [Middleware] CORS 설정 app.use(corsMiddleware); @@ -95,8 +95,8 @@ app.use(errorHandler); // express 서버 시작 const serverHttp = http .createServer(app) - .listen(httpPort, () => - logger.info(`Express server started from port ${httpPort}`) + .listen(config.port, () => + logger.info(`Express server started from port ${config.port}`) ); // socket.io 서버 시작 diff --git a/src/loadenv.ts b/src/loadenv.ts index c70bb4fa..6d6c508b 100644 --- a/src/loadenv.ts +++ b/src/loadenv.ts @@ -2,6 +2,7 @@ import dotenv from "dotenv"; import { type Algorithm } from "jsonwebtoken"; import { type ServiceAccount } from "firebase-admin"; +import exp from "constants"; if (process.env.NODE_ENV === undefined) { // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. @@ -18,52 +19,55 @@ if (process.env.DB_PATH === undefined) { process.exit(1); } -export const nodeEnv = process.env.NODE_ENV; // required ("production" or "development" or "test") -export const mongo = process.env.DB_PATH; // required -export const session = { - secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", // optional - expiry: 14 * 24 * 3600 * 1000, // 14일, ms 단위입니다. -}; -export const redis = process.env.REDIS_PATH; // optional -export const sparcssso = { - id: process.env.SPARCSSSO_CLIENT_ID || "", // optional - key: process.env.SPARCSSSO_CLIENT_KEY || "", // optional -}; -export const port = process.env.PORT ? parseInt(process.env.PORT) : 80; // optional (default = 80) -export const corsWhiteList = (process.env.CORS_WHITELIST && - (JSON.parse(process.env.CORS_WHITELIST) as string[])) || [true]; // optional (default = [true]) -export const aws = { - accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, // required - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, // required - s3BucketName: process.env.AWS_S3_BUCKET_NAME as string, // required - s3Url: - process.env.AWS_S3_URL || - `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional -}; -export const jwt = { - secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", - option: { - algorithm: "HS256" as Algorithm, - // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. - // See https://github.com/sparcs-kaist/taxi-back/issues/415 - issuer: process.env.FRONT_URL || "http://localhost:3000", // optional (default = "http://localhost:3000") +const config = { + nodeEnv: process.env.NODE_ENV, + mongoUrl: process.env.DB_PATH, + session: { + secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", + expiry: 14 * 24 * 3600 * 1000, + }, + redisUrl: process.env.REDIS_PATH, + sparcssso: { + id: process.env.SPARCSSSO_CLIENT_ID || "", + key: process.env.SPARCSSSO_CLIENT_KEY || "", + }, + port: process.env.PORT ? parseInt(process.env.PORT) : 80, + corsWhiteList: (process.env.CORS_WHITELIST && + (JSON.parse(process.env.CORS_WHITELIST) as string[])) || [true], + aws: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, + s3BucketName: process.env.AWS_S3_BUCKET_NAME as string, + s3Url: + process.env.AWS_S3_URL || + `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, + }, + jwt: { + secretKey: process.env.JWT_SECRET || "TAXI_JWT_KEY", + option: { + algorithm: "HS256" as Algorithm, + // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. + // See https://github.com/sparcs-kaist/taxi-back/issues/415 + issuer: process.env.FRONT_URL || "http://localhost:3000", + }, + TOKEN_EXPIRED: -3, + TOKEN_INVALID: -2, + }, + + googleApplicationCredentials: + process.env.GOOGLE_APPLICATION_CREDENTIALS && + (JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS) as ServiceAccount), + testAccounts: + (process.env.TEST_ACCOUNTS && + (JSON.parse(process.env.TEST_ACCOUNTS) as string[])) || + [], + slackWebhookUrl: { + report: process.env.SLACK_REPORT_WEBHOOK_URL || "", + }, + naverMap: { + apiId: process.env.NAVER_MAP_API_ID || "", + apiKey: process.env.NAVER_MAP_API_KEY || "", }, - TOKEN_EXPIRED: -3, - TOKEN_INVALID: -2, -}; -export const googleApplicationCredentials = - process.env.GOOGLE_APPLICATION_CREDENTIALS && - (JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS) as ServiceAccount); // optional -export const testAccounts = - (process.env.TEST_ACCOUNTS && - (JSON.parse(process.env.TEST_ACCOUNTS) as string[])) || - []; // optional -export const slackWebhookUrl = { - report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional -}; -// export const eventConfig = -// process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG); // optional -export const naverMap = { - apiId: process.env.NAVER_MAP_API_ID || "", // optional - apiKey: process.env.NAVER_MAP_API_KEY || "", // optional }; + +export default config; \ No newline at end of file diff --git a/src/middlewares/cors.ts b/src/middlewares/cors.ts index 63559565..f8678f7f 100644 --- a/src/middlewares/cors.ts +++ b/src/middlewares/cors.ts @@ -1,8 +1,8 @@ import cors from "cors"; -import { corsWhiteList } from "@/loadenv"; +import config from "@/loadenv"; const corsMiddleware = cors({ - origin: corsWhiteList, + origin: config.corsWhiteList, credentials: true, exposedHeaders: ["Date"], }); diff --git a/src/middlewares/session.ts b/src/middlewares/session.ts index 8cf0dcf8..9b267b4a 100644 --- a/src/middlewares/session.ts +++ b/src/middlewares/session.ts @@ -1,5 +1,5 @@ import expressSession from "express-session"; -import { nodeEnv, session as sessionConfig } from "@/loadenv"; +import config from "@/loadenv"; import { type LoginInfo } from "@/modules/auths/login"; import sessionStore from "@/modules/stores/sessionStore"; @@ -26,14 +26,14 @@ declare module "express-session" { } const sessionMiddleware = expressSession({ - secret: sessionConfig.secret, + secret: config.session.secret, resave: false, saveUninitialized: false, store: sessionStore, cookie: { - maxAge: sessionConfig.expiry, + maxAge: config.session.expiry, // nodeEnv가 production일 때만 secure cookie를 사용합니다. - secure: nodeEnv === "production", + secure: config.nodeEnv === "production", }, }); diff --git a/src/modules/auths/jwt.ts b/src/modules/auths/jwt.ts index ee009330..a32cd47b 100644 --- a/src/modules/auths/jwt.ts +++ b/src/modules/auths/jwt.ts @@ -1,7 +1,7 @@ import jwt, { type SignOptions } from "jsonwebtoken"; -import { jwt as jwtConfig } from "@/loadenv"; +import config from "@/loadenv"; -const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = jwtConfig; +const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = config.jwt; type TokenType = "access" | "refresh"; diff --git a/src/modules/auths/login.ts b/src/modules/auths/login.ts index a55677b6..f026215b 100644 --- a/src/modules/auths/login.ts +++ b/src/modules/auths/login.ts @@ -1,5 +1,5 @@ import { type Request } from "express"; -import { session as sessionConfig } from "@/loadenv"; +import config from "@/loadenv"; import logger from "@/modules/logger"; export interface LoginInfo { @@ -16,7 +16,7 @@ export const getLoginInfo = (req: Request) => { const timeFlow = Date.now() - time; // 14일이 지난 세션에 대해서는 로그인 정보를 반환하지 않습니다. // 세션은 새로운 요청 시 갱신되지 않습니다. - if (timeFlow > sessionConfig.expiry) { + if (timeFlow > config.session.expiry) { return { id: undefined, sid: undefined, oid: undefined, name: undefined }; } return { id, sid, oid, name }; diff --git a/src/modules/fcm.ts b/src/modules/fcm.ts index ae5abf5b..44b9979a 100644 --- a/src/modules/fcm.ts +++ b/src/modules/fcm.ts @@ -1,6 +1,6 @@ import firebaseAdmin from "firebase-admin"; import { type SendResponse, getMessaging } from "firebase-admin/messaging"; -import { googleApplicationCredentials } from "@/loadenv"; +import config from "@/loadenv"; import logger from "@/modules/logger"; import { deviceTokenModel, @@ -13,9 +13,11 @@ import { type ChatType } from "@/types/mongo"; * credential을 등록합니다. */ export const initializeApp = () => { - if (googleApplicationCredentials) { + if (config.googleApplicationCredentials) { firebaseAdmin.initializeApp({ - credential: firebaseAdmin.credential.cert(googleApplicationCredentials), + credential: firebaseAdmin.credential.cert( + config.googleApplicationCredentials + ), }); } else { logger.error( diff --git a/src/modules/logger.ts b/src/modules/logger.ts index b4b91610..5a2c0424 100644 --- a/src/modules/logger.ts +++ b/src/modules/logger.ts @@ -2,7 +2,7 @@ import path from "path"; import { createLogger, format, transports } from "winston"; import DailyRotateFileTransport from "winston-daily-rotate-file"; -import { nodeEnv } from "@/loadenv"; +import config from "@/loadenv"; // logger에서 사용할 포맷들을 정의합니다. const baseFormat = format.combine( @@ -50,7 +50,7 @@ const consoleTransport = new transports.Console(); * @method error(message: string, callback: winston.LogCallback) - 오류 메시지를 기록하기 위해 사용합니다. */ const logger = - nodeEnv === "production" + config.nodeEnv === "production" ? // "production" 환경에서 사용되는 Logger 객체 createLogger({ level: "info", diff --git a/src/modules/slackNotification.ts b/src/modules/slackNotification.ts index bd3b9837..797aab30 100644 --- a/src/modules/slackNotification.ts +++ b/src/modules/slackNotification.ts @@ -1,18 +1,19 @@ import axios from "axios"; -import { nodeEnv, slackWebhookUrl as slackUrl } from "@/loadenv"; +import config from "@/loadenv"; import logger from "@/modules/logger"; import { type Report } from "@/types/mongo"; export const sendTextToReportChannel = (text: string) => { - if (!slackUrl.report) return; + if (!config.slackWebhookUrl.report) return; const data = { - text: nodeEnv === "production" ? text : `(${nodeEnv}) ${text}`, // Production 환경이 아닌 경우, 환경 이름을 붙여서 전송합니다. + text: + config.nodeEnv === "production" ? text : `(${config.nodeEnv}) ${text}`, // Production 환경이 아닌 경우, 환경 이름을 붙여서 전송합니다. }; - const config = { headers: { "Content-Type": "application/json" } }; + const axiosConfig = { headers: { "Content-Type": "application/json" } }; axios - .post(slackUrl.report, data, config) + .post(config.slackWebhookUrl.report, data, axiosConfig) .then(() => { logger.info("Slack webhook sent successfully"); }) diff --git a/src/modules/stores/aws.ts b/src/modules/stores/aws.ts index 4c1c6885..da0ad821 100644 --- a/src/modules/stores/aws.ts +++ b/src/modules/stores/aws.ts @@ -1,5 +1,5 @@ import AWS from "aws-sdk"; -import { aws as awsEnv } from "@/loadenv"; +import config from "@/loadenv"; AWS.config.update({ region: "ap-northeast-2", @@ -16,7 +16,7 @@ export const getList = ( ) => { s3.listObjects( { - Bucket: awsEnv.s3BucketName, + Bucket: config.aws.s3BucketName, Prefix: directoryPath, }, (err, data) => { @@ -31,7 +31,7 @@ export const getUploadPUrlPut = ( contentType: string = "image/png" ) => { const presignedUrl = s3.getSignedUrl("putObject", { - Bucket: awsEnv.s3BucketName, + Bucket: config.aws.s3BucketName, Key: filePath, ContentType: contentType, Expires: 60, // 1 min @@ -47,7 +47,7 @@ export const getUploadPUrlPost = ( ) => { s3.createPresignedPost( { - Bucket: awsEnv.s3BucketName, + Bucket: config.aws.s3BucketName, Expires: 60, // 1 min Conditions: [ { key: filePath }, @@ -68,7 +68,7 @@ export const deleteObject = ( ) => { s3.deleteObject( { - Bucket: awsEnv.s3BucketName, + Bucket: config.aws.s3BucketName, Key: filePath, }, (err, data) => { @@ -84,7 +84,7 @@ export const foundObject = ( ) => { s3.headObject( { - Bucket: awsEnv.s3BucketName, + Bucket: config.aws.s3BucketName, Key: filePath, }, (err, data) => { @@ -95,5 +95,5 @@ export const foundObject = ( // function to return full URL of the object export const getS3Url = (filePath: string) => { - return `${awsEnv.s3Url}${filePath}`; + return `${config.aws.s3Url}${filePath}`; }; diff --git a/src/modules/stores/sessionStore.ts b/src/modules/stores/sessionStore.ts index 11ec9a78..f4e89421 100644 --- a/src/modules/stores/sessionStore.ts +++ b/src/modules/stores/sessionStore.ts @@ -1,18 +1,14 @@ import MongoStore from "connect-mongo"; import RedisStore from "connect-redis"; import redis from "redis"; -import { - redis as redisUrl, - mongo as mongoUrl, - session as sessionConfig, -} from "@/loadenv"; +import config from "@/loadenv"; import logger from "@/modules/logger"; const getSessionStore = () => { // 환경변수 REDIS_PATH 유무에 따라 session 저장 방식이 변경됩니다. - if (redisUrl) { + if (config.redisUrl) { const client = redis.createClient({ - url: redisUrl, + url: config.redisUrl, }); // redis client 연결 성공 시 로그를 출력합니다. @@ -26,9 +22,9 @@ const getSessionStore = () => { }); client.connect().catch(logger.error); - return new RedisStore({ client, ttl: sessionConfig.expiry }); + return new RedisStore({ client, ttl: config.session.expiry }); } else { - return MongoStore.create({ mongoUrl }); + return MongoStore.create({ mongoUrl: config.mongoUrl }); } }; diff --git a/src/sampleGenerator/index.js b/src/sampleGenerator/index.js index c1168dab..68e2a532 100644 --- a/src/sampleGenerator/index.js +++ b/src/sampleGenerator/index.js @@ -5,7 +5,7 @@ const { generateChats, } = require("./src/testData"); const { connectDatabase } = require("../modules/stores/mongo"); -const { mongo: mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); +const { mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); const database = connectDatabase(mongoUrl); diff --git a/src/schedules/index.ts b/src/schedules/index.ts index 1908fadd..cf864c90 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -4,12 +4,12 @@ import notifyBeforeDepart from "./notifyBeforeDepart"; import notifyAfterArrival from "./notifyAfterArrival"; import updateMajorTaxiFare from "./updateMajorTaxiFare"; import updateMinorTaxiFare from "./updateMinorTaxiFare"; -import { naverMap } from "@/loadenv"; +import config from "@/loadenv"; const registerSchedules = (app: Express) => { cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); cron.schedule("*/10 * * * *", notifyAfterArrival(app)); - if (naverMap.apiId && naverMap.apiKey) { + if (config.naverMap.apiId && config.naverMap.apiKey) { cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app)); cron.schedule("0 18 * * *", updateMinorTaxiFare(app)); } From 3ce54065720747f35a8974e2d90bb7bdc9fa7493 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Nov 2024 22:50:41 +0900 Subject: [PATCH 121/151] Refactor: middlewares --- src/middlewares/auth.ts | 19 +++++++++---------- src/middlewares/authAdmin.ts | 24 +++++++++++------------- src/middlewares/errorHandler.ts | 17 +++++++---------- src/middlewares/information.ts | 8 ++------ src/middlewares/limitRate.ts | 4 ++-- src/middlewares/originValidator.ts | 14 +++++--------- src/middlewares/session.ts | 2 +- src/middlewares/validator.ts | 14 +++++--------- 8 files changed, 42 insertions(+), 60 deletions(-) diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index 535ed1ec..a0215b9d 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -1,18 +1,17 @@ // 로그인된 상태에만 접근할 수 있는 라우터(rooms)를 위한 미들웨어입니다. -import { type Request, type Response, type NextFunction } from "express"; +import type { RequestHandler } from "express"; import { isLogin, getLoginInfo } from "@/modules/auths/login"; -const authMiddleware = (req: Request, res: Response, next: NextFunction) => { - if (!isLogin(req)) { - res.status(403).json({ +const authMiddleware: RequestHandler = (req, res, next) => { + if (!isLogin(req)) + return res.status(403).json({ error: "not logged in", }); - } else { - const { id, oid } = getLoginInfo(req); - req.userId = id; - req.userOid = oid; - next(); - } + + const { id, oid } = getLoginInfo(req); + req.userId = id; + req.userOid = oid; + next(); }; export default authMiddleware; diff --git a/src/middlewares/authAdmin.ts b/src/middlewares/authAdmin.ts index af9c7b5d..cc79ef29 100644 --- a/src/middlewares/authAdmin.ts +++ b/src/middlewares/authAdmin.ts @@ -1,34 +1,32 @@ // 관리자 유무를 확인하기 위한 미들웨어입니다. -import { type Request, type Response, type NextFunction } from "express"; +import type { RequestHandler } from "express"; import { isLogin, getLoginInfo } from "@/modules/auths/login"; import { userModel, adminIPWhitelistModel } from "@/modules/stores/mongo"; -const authAdminMiddleware = async ( - req: Request, - res: Response, - next: NextFunction -) => { +const authAdminMiddleware: RequestHandler = async (req, res, next) => { + const redirectUrl = req.origin ?? "/"; + try { // 로그인 여부를 확인 - if (!isLogin(req)) return res.redirect(req.origin ?? "/"); + if (!isLogin(req)) return res.redirect(redirectUrl); // 관리자 유무를 확인 const { id } = getLoginInfo(req); const user = await userModel.findOne({ id }); - if (!user?.isAdmin) return res.redirect(req.origin ?? "/"); + if (!user?.isAdmin) return res.redirect(redirectUrl); // 접속한 IP가 화이트리스트에 있는지 확인 const ipWhitelist = await adminIPWhitelistModel.find({}); - if (!req.clientIP) return res.redirect(req.origin ?? "/"); + if (!req.clientIP) return res.redirect(redirectUrl); if ( ipWhitelist.length > 0 && - ipWhitelist.map((x: any) => x.ip).indexOf(req.clientIP) < 0 // TODO: Remove any + ipWhitelist.map((x) => x.ip).indexOf(req.clientIP) < 0 ) - return res.redirect(req.origin ?? "/"); + return res.redirect(redirectUrl); - return next(); + next(); } catch (e) { - return res.redirect(req.origin ?? "/"); + return res.redirect(redirectUrl); } }; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index e0a7ba72..f130caa8 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,4 +1,4 @@ -import { type Request, type Response, type NextFunction } from "express"; +import type { ErrorRequestHandler } from "express"; import logger from "@/modules/logger"; /** @@ -11,20 +11,17 @@ import logger from "@/modules/logger"; * @param res - 응답 객체 * @param next - 다음 미들웨어 함수. Express에서는 next 함수에 err 인자를 넘겨주면 기본 global error handler가 호출됩니다. */ -const errorHandler = ( - err: Error, - req: Request, - res: Response, - next: NextFunction -) => { +const errorHandler: ErrorRequestHandler = (err, req, res, next) => { + logger.error(err); + // 이미 클라이언트에 HTTP 응답 헤더를 전송한 경우, 응답 헤더를 다시 전송하지 않아야 합니다. // 클라이언트에게 스트리밍 형태로 응답을 전송하는 도중 오류가 발생하는 경우가 여기에 해당합니다. // 이럴 때 기본 global error handler를 호출하면 기본 global error handler가 클라이언트와의 연결을 종료시켜 줍니다. - logger.error(err); if (res.headersSent) { - return next(err); + next(err); + } else { + return res.status(500).send("internal server error"); } - return res.status(500).send("internal server error"); }; export default errorHandler; diff --git a/src/middlewares/information.ts b/src/middlewares/information.ts index 0006ea40..91b3d66f 100644 --- a/src/middlewares/information.ts +++ b/src/middlewares/information.ts @@ -1,10 +1,6 @@ -import { type Request, type Response, type NextFunction } from "express"; +import type { RequestHandler } from "express"; -const informationMiddleware = ( - req: Request, - res: Response, - next: NextFunction -) => { +const informationMiddleware: RequestHandler = (req, res, next) => { req.clientIP = (req.headers["x-forwarded-for"] as string | undefined) || req.connection.remoteAddress; diff --git a/src/middlewares/limitRate.ts b/src/middlewares/limitRate.ts index d137892b..18a3203b 100644 --- a/src/middlewares/limitRate.ts +++ b/src/middlewares/limitRate.ts @@ -1,10 +1,10 @@ import rateLimit from "express-rate-limit"; -const limiter = rateLimit({ +const limitRateMiddleware = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 1500, // Limit each IP to 1500 requests per `window` (here, per 15 minutes) standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); -export default limiter; +export default limitRateMiddleware; diff --git a/src/middlewares/originValidator.ts b/src/middlewares/originValidator.ts index 3927d46e..330226cc 100644 --- a/src/middlewares/originValidator.ts +++ b/src/middlewares/originValidator.ts @@ -1,21 +1,17 @@ -import { type Request, type Response, type NextFunction } from "express"; +import type { RequestHandler } from "express"; -const originValidatorMiddleware = ( - req: Request, - res: Response, - next: NextFunction -) => { +const originValidatorMiddleware: RequestHandler = (req, res, next) => { req.origin = req.headers.origin || req.headers.referer || req.session?.loginAfterState?.redirectOrigin; // sparcssso/callback 요청은 헤더에 origin이 없음 - if (!req.origin) { + if (!req.origin) return res.status(400).json({ error: "Bad Request : request must have origin in header", }); - } - return next(); + + next(); }; export default originValidatorMiddleware; diff --git a/src/middlewares/session.ts b/src/middlewares/session.ts index 8cf0dcf8..3486154e 100644 --- a/src/middlewares/session.ts +++ b/src/middlewares/session.ts @@ -1,6 +1,6 @@ import expressSession from "express-session"; import { nodeEnv, session as sessionConfig } from "@/loadenv"; -import { type LoginInfo } from "@/modules/auths/login"; +import type { LoginInfo } from "@/modules/auths/login"; import sessionStore from "@/modules/stores/sessionStore"; // 세션에 저장할 데이터 타입을 지정합니다. diff --git a/src/middlewares/validator.ts b/src/middlewares/validator.ts index b4e1b0cf..aafb165a 100644 --- a/src/middlewares/validator.ts +++ b/src/middlewares/validator.ts @@ -1,18 +1,14 @@ -import { type Request, type Response, type NextFunction } from "express"; +import type { RequestHandler } from "express"; import { validationResult } from "express-validator"; -const validatorMiddleware = ( - req: Request, - res: Response, - next: NextFunction -) => { +const validatorMiddleware: RequestHandler = (req, res, next) => { const validationErrors = validationResult(req); - if (!validationErrors.isEmpty()) { + if (!validationErrors.isEmpty()) return res.status(400).json({ error: "validation : bad request", }); - } - return next(); + + next(); }; export default validatorMiddleware; From d90f28c56b5ff6e97b95d2a14af5d7dbb2cbc086 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Nov 2024 23:26:00 +0900 Subject: [PATCH 122/151] =?UTF-8?q?Revert=20"Fix:=20loadenv=20=EB=A5=BC=20?= =?UTF-8?q?As-Is=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8D=98?= =?UTF-8?q?=20Nested=20object=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 7e17d5fa8bd9a1148d809c8feb24f2b2c28e0805. --- scripts/profileImageUrlUpdater.js | 2 +- src/index.ts | 10 +-- src/loadenv.ts | 98 ++++++++++++++---------------- src/middlewares/cors.ts | 4 +- src/middlewares/session.ts | 8 +-- src/modules/auths/jwt.ts | 4 +- src/modules/auths/login.ts | 4 +- src/modules/fcm.ts | 8 +-- src/modules/logger.ts | 4 +- src/modules/slackNotification.ts | 11 ++-- src/modules/stores/aws.ts | 14 ++--- src/modules/stores/sessionStore.ts | 14 +++-- src/sampleGenerator/index.js | 2 +- src/schedules/index.ts | 4 +- 14 files changed, 92 insertions(+), 95 deletions(-) diff --git a/scripts/profileImageUrlUpdater.js b/scripts/profileImageUrlUpdater.js index fdfa671e..2b35bf1a 100644 --- a/scripts/profileImageUrlUpdater.js +++ b/scripts/profileImageUrlUpdater.js @@ -2,7 +2,7 @@ // https://github.com/sparcs-kaist/taxi-back/issues/173 const { MongoClient } = require("mongodb"); -const { mongoUrl, aws: awsEnv } = require("../loadenv"); // FIXME: 올바른 경로로 수정해야 합니다. +const { mongo: mongoUrl, aws: awsEnv } = require("../loadenv"); // FIXME: 올바른 경로로 수정해야 합니다. const time = Date.now(); diff --git a/src/index.ts b/src/index.ts index 4aebcc46..f741d146 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import express from "express"; import cookieParser from "cookie-parser"; import http from "http"; -import config from "@/loadenv"; +import { nodeEnv, mongo as mongoUrl, port as httpPort } from "@/loadenv"; import { corsMiddleware, sessionMiddleware, @@ -40,14 +40,14 @@ initializeApp(); const app = express(); // 데이터베이스 연결 -connectDatabase(config.mongoUrl); +connectDatabase(mongoUrl); // [Middleware] request body 파싱 app.use(express.urlencoded({ extended: false })); app.use(express.json()); // reverse proxy가 설정한 헤더를 신뢰합니다. -if (config.nodeEnv === "production") app.set("trust proxy", 2); +if (nodeEnv === "production") app.set("trust proxy", 2); // [Middleware] CORS 설정 app.use(corsMiddleware); @@ -95,8 +95,8 @@ app.use(errorHandler); // express 서버 시작 const serverHttp = http .createServer(app) - .listen(config.port, () => - logger.info(`Express server started from port ${config.port}`) + .listen(httpPort, () => + logger.info(`Express server started from port ${httpPort}`) ); // socket.io 서버 시작 diff --git a/src/loadenv.ts b/src/loadenv.ts index 6d6c508b..c70bb4fa 100644 --- a/src/loadenv.ts +++ b/src/loadenv.ts @@ -2,7 +2,6 @@ import dotenv from "dotenv"; import { type Algorithm } from "jsonwebtoken"; import { type ServiceAccount } from "firebase-admin"; -import exp from "constants"; if (process.env.NODE_ENV === undefined) { // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. @@ -19,55 +18,52 @@ if (process.env.DB_PATH === undefined) { process.exit(1); } -const config = { - nodeEnv: process.env.NODE_ENV, - mongoUrl: process.env.DB_PATH, - session: { - secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", - expiry: 14 * 24 * 3600 * 1000, - }, - redisUrl: process.env.REDIS_PATH, - sparcssso: { - id: process.env.SPARCSSSO_CLIENT_ID || "", - key: process.env.SPARCSSSO_CLIENT_KEY || "", - }, - port: process.env.PORT ? parseInt(process.env.PORT) : 80, - corsWhiteList: (process.env.CORS_WHITELIST && - (JSON.parse(process.env.CORS_WHITELIST) as string[])) || [true], - aws: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, - s3BucketName: process.env.AWS_S3_BUCKET_NAME as string, - s3Url: - process.env.AWS_S3_URL || - `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, - }, - jwt: { - secretKey: process.env.JWT_SECRET || "TAXI_JWT_KEY", - option: { - algorithm: "HS256" as Algorithm, - // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. - // See https://github.com/sparcs-kaist/taxi-back/issues/415 - issuer: process.env.FRONT_URL || "http://localhost:3000", - }, - TOKEN_EXPIRED: -3, - TOKEN_INVALID: -2, - }, - - googleApplicationCredentials: - process.env.GOOGLE_APPLICATION_CREDENTIALS && - (JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS) as ServiceAccount), - testAccounts: - (process.env.TEST_ACCOUNTS && - (JSON.parse(process.env.TEST_ACCOUNTS) as string[])) || - [], - slackWebhookUrl: { - report: process.env.SLACK_REPORT_WEBHOOK_URL || "", - }, - naverMap: { - apiId: process.env.NAVER_MAP_API_ID || "", - apiKey: process.env.NAVER_MAP_API_KEY || "", +export const nodeEnv = process.env.NODE_ENV; // required ("production" or "development" or "test") +export const mongo = process.env.DB_PATH; // required +export const session = { + secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", // optional + expiry: 14 * 24 * 3600 * 1000, // 14일, ms 단위입니다. +}; +export const redis = process.env.REDIS_PATH; // optional +export const sparcssso = { + id: process.env.SPARCSSSO_CLIENT_ID || "", // optional + key: process.env.SPARCSSSO_CLIENT_KEY || "", // optional +}; +export const port = process.env.PORT ? parseInt(process.env.PORT) : 80; // optional (default = 80) +export const corsWhiteList = (process.env.CORS_WHITELIST && + (JSON.parse(process.env.CORS_WHITELIST) as string[])) || [true]; // optional (default = [true]) +export const aws = { + accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, // required + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, // required + s3BucketName: process.env.AWS_S3_BUCKET_NAME as string, // required + s3Url: + process.env.AWS_S3_URL || + `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional +}; +export const jwt = { + secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", + option: { + algorithm: "HS256" as Algorithm, + // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. + // See https://github.com/sparcs-kaist/taxi-back/issues/415 + issuer: process.env.FRONT_URL || "http://localhost:3000", // optional (default = "http://localhost:3000") }, + TOKEN_EXPIRED: -3, + TOKEN_INVALID: -2, +}; +export const googleApplicationCredentials = + process.env.GOOGLE_APPLICATION_CREDENTIALS && + (JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS) as ServiceAccount); // optional +export const testAccounts = + (process.env.TEST_ACCOUNTS && + (JSON.parse(process.env.TEST_ACCOUNTS) as string[])) || + []; // optional +export const slackWebhookUrl = { + report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional +}; +// export const eventConfig = +// process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG); // optional +export const naverMap = { + apiId: process.env.NAVER_MAP_API_ID || "", // optional + apiKey: process.env.NAVER_MAP_API_KEY || "", // optional }; - -export default config; \ No newline at end of file diff --git a/src/middlewares/cors.ts b/src/middlewares/cors.ts index f8678f7f..63559565 100644 --- a/src/middlewares/cors.ts +++ b/src/middlewares/cors.ts @@ -1,8 +1,8 @@ import cors from "cors"; -import config from "@/loadenv"; +import { corsWhiteList } from "@/loadenv"; const corsMiddleware = cors({ - origin: config.corsWhiteList, + origin: corsWhiteList, credentials: true, exposedHeaders: ["Date"], }); diff --git a/src/middlewares/session.ts b/src/middlewares/session.ts index 3615fee8..3486154e 100644 --- a/src/middlewares/session.ts +++ b/src/middlewares/session.ts @@ -1,5 +1,5 @@ import expressSession from "express-session"; -import config from "@/loadenv"; +import { nodeEnv, session as sessionConfig } from "@/loadenv"; import type { LoginInfo } from "@/modules/auths/login"; import sessionStore from "@/modules/stores/sessionStore"; @@ -26,14 +26,14 @@ declare module "express-session" { } const sessionMiddleware = expressSession({ - secret: config.session.secret, + secret: sessionConfig.secret, resave: false, saveUninitialized: false, store: sessionStore, cookie: { - maxAge: config.session.expiry, + maxAge: sessionConfig.expiry, // nodeEnv가 production일 때만 secure cookie를 사용합니다. - secure: config.nodeEnv === "production", + secure: nodeEnv === "production", }, }); diff --git a/src/modules/auths/jwt.ts b/src/modules/auths/jwt.ts index a32cd47b..ee009330 100644 --- a/src/modules/auths/jwt.ts +++ b/src/modules/auths/jwt.ts @@ -1,7 +1,7 @@ import jwt, { type SignOptions } from "jsonwebtoken"; -import config from "@/loadenv"; +import { jwt as jwtConfig } from "@/loadenv"; -const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = config.jwt; +const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = jwtConfig; type TokenType = "access" | "refresh"; diff --git a/src/modules/auths/login.ts b/src/modules/auths/login.ts index f026215b..a55677b6 100644 --- a/src/modules/auths/login.ts +++ b/src/modules/auths/login.ts @@ -1,5 +1,5 @@ import { type Request } from "express"; -import config from "@/loadenv"; +import { session as sessionConfig } from "@/loadenv"; import logger from "@/modules/logger"; export interface LoginInfo { @@ -16,7 +16,7 @@ export const getLoginInfo = (req: Request) => { const timeFlow = Date.now() - time; // 14일이 지난 세션에 대해서는 로그인 정보를 반환하지 않습니다. // 세션은 새로운 요청 시 갱신되지 않습니다. - if (timeFlow > config.session.expiry) { + if (timeFlow > sessionConfig.expiry) { return { id: undefined, sid: undefined, oid: undefined, name: undefined }; } return { id, sid, oid, name }; diff --git a/src/modules/fcm.ts b/src/modules/fcm.ts index 44b9979a..ae5abf5b 100644 --- a/src/modules/fcm.ts +++ b/src/modules/fcm.ts @@ -1,6 +1,6 @@ import firebaseAdmin from "firebase-admin"; import { type SendResponse, getMessaging } from "firebase-admin/messaging"; -import config from "@/loadenv"; +import { googleApplicationCredentials } from "@/loadenv"; import logger from "@/modules/logger"; import { deviceTokenModel, @@ -13,11 +13,9 @@ import { type ChatType } from "@/types/mongo"; * credential을 등록합니다. */ export const initializeApp = () => { - if (config.googleApplicationCredentials) { + if (googleApplicationCredentials) { firebaseAdmin.initializeApp({ - credential: firebaseAdmin.credential.cert( - config.googleApplicationCredentials - ), + credential: firebaseAdmin.credential.cert(googleApplicationCredentials), }); } else { logger.error( diff --git a/src/modules/logger.ts b/src/modules/logger.ts index 5a2c0424..b4b91610 100644 --- a/src/modules/logger.ts +++ b/src/modules/logger.ts @@ -2,7 +2,7 @@ import path from "path"; import { createLogger, format, transports } from "winston"; import DailyRotateFileTransport from "winston-daily-rotate-file"; -import config from "@/loadenv"; +import { nodeEnv } from "@/loadenv"; // logger에서 사용할 포맷들을 정의합니다. const baseFormat = format.combine( @@ -50,7 +50,7 @@ const consoleTransport = new transports.Console(); * @method error(message: string, callback: winston.LogCallback) - 오류 메시지를 기록하기 위해 사용합니다. */ const logger = - config.nodeEnv === "production" + nodeEnv === "production" ? // "production" 환경에서 사용되는 Logger 객체 createLogger({ level: "info", diff --git a/src/modules/slackNotification.ts b/src/modules/slackNotification.ts index 797aab30..bd3b9837 100644 --- a/src/modules/slackNotification.ts +++ b/src/modules/slackNotification.ts @@ -1,19 +1,18 @@ import axios from "axios"; -import config from "@/loadenv"; +import { nodeEnv, slackWebhookUrl as slackUrl } from "@/loadenv"; import logger from "@/modules/logger"; import { type Report } from "@/types/mongo"; export const sendTextToReportChannel = (text: string) => { - if (!config.slackWebhookUrl.report) return; + if (!slackUrl.report) return; const data = { - text: - config.nodeEnv === "production" ? text : `(${config.nodeEnv}) ${text}`, // Production 환경이 아닌 경우, 환경 이름을 붙여서 전송합니다. + text: nodeEnv === "production" ? text : `(${nodeEnv}) ${text}`, // Production 환경이 아닌 경우, 환경 이름을 붙여서 전송합니다. }; - const axiosConfig = { headers: { "Content-Type": "application/json" } }; + const config = { headers: { "Content-Type": "application/json" } }; axios - .post(config.slackWebhookUrl.report, data, axiosConfig) + .post(slackUrl.report, data, config) .then(() => { logger.info("Slack webhook sent successfully"); }) diff --git a/src/modules/stores/aws.ts b/src/modules/stores/aws.ts index da0ad821..4c1c6885 100644 --- a/src/modules/stores/aws.ts +++ b/src/modules/stores/aws.ts @@ -1,5 +1,5 @@ import AWS from "aws-sdk"; -import config from "@/loadenv"; +import { aws as awsEnv } from "@/loadenv"; AWS.config.update({ region: "ap-northeast-2", @@ -16,7 +16,7 @@ export const getList = ( ) => { s3.listObjects( { - Bucket: config.aws.s3BucketName, + Bucket: awsEnv.s3BucketName, Prefix: directoryPath, }, (err, data) => { @@ -31,7 +31,7 @@ export const getUploadPUrlPut = ( contentType: string = "image/png" ) => { const presignedUrl = s3.getSignedUrl("putObject", { - Bucket: config.aws.s3BucketName, + Bucket: awsEnv.s3BucketName, Key: filePath, ContentType: contentType, Expires: 60, // 1 min @@ -47,7 +47,7 @@ export const getUploadPUrlPost = ( ) => { s3.createPresignedPost( { - Bucket: config.aws.s3BucketName, + Bucket: awsEnv.s3BucketName, Expires: 60, // 1 min Conditions: [ { key: filePath }, @@ -68,7 +68,7 @@ export const deleteObject = ( ) => { s3.deleteObject( { - Bucket: config.aws.s3BucketName, + Bucket: awsEnv.s3BucketName, Key: filePath, }, (err, data) => { @@ -84,7 +84,7 @@ export const foundObject = ( ) => { s3.headObject( { - Bucket: config.aws.s3BucketName, + Bucket: awsEnv.s3BucketName, Key: filePath, }, (err, data) => { @@ -95,5 +95,5 @@ export const foundObject = ( // function to return full URL of the object export const getS3Url = (filePath: string) => { - return `${config.aws.s3Url}${filePath}`; + return `${awsEnv.s3Url}${filePath}`; }; diff --git a/src/modules/stores/sessionStore.ts b/src/modules/stores/sessionStore.ts index f4e89421..11ec9a78 100644 --- a/src/modules/stores/sessionStore.ts +++ b/src/modules/stores/sessionStore.ts @@ -1,14 +1,18 @@ import MongoStore from "connect-mongo"; import RedisStore from "connect-redis"; import redis from "redis"; -import config from "@/loadenv"; +import { + redis as redisUrl, + mongo as mongoUrl, + session as sessionConfig, +} from "@/loadenv"; import logger from "@/modules/logger"; const getSessionStore = () => { // 환경변수 REDIS_PATH 유무에 따라 session 저장 방식이 변경됩니다. - if (config.redisUrl) { + if (redisUrl) { const client = redis.createClient({ - url: config.redisUrl, + url: redisUrl, }); // redis client 연결 성공 시 로그를 출력합니다. @@ -22,9 +26,9 @@ const getSessionStore = () => { }); client.connect().catch(logger.error); - return new RedisStore({ client, ttl: config.session.expiry }); + return new RedisStore({ client, ttl: sessionConfig.expiry }); } else { - return MongoStore.create({ mongoUrl: config.mongoUrl }); + return MongoStore.create({ mongoUrl }); } }; diff --git a/src/sampleGenerator/index.js b/src/sampleGenerator/index.js index 68e2a532..c1168dab 100644 --- a/src/sampleGenerator/index.js +++ b/src/sampleGenerator/index.js @@ -5,7 +5,7 @@ const { generateChats, } = require("./src/testData"); const { connectDatabase } = require("../modules/stores/mongo"); -const { mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); +const { mongo: mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); const database = connectDatabase(mongoUrl); diff --git a/src/schedules/index.ts b/src/schedules/index.ts index cf864c90..1908fadd 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -4,12 +4,12 @@ import notifyBeforeDepart from "./notifyBeforeDepart"; import notifyAfterArrival from "./notifyAfterArrival"; import updateMajorTaxiFare from "./updateMajorTaxiFare"; import updateMinorTaxiFare from "./updateMinorTaxiFare"; -import config from "@/loadenv"; +import { naverMap } from "@/loadenv"; const registerSchedules = (app: Express) => { cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); cron.schedule("*/10 * * * *", notifyAfterArrival(app)); - if (config.naverMap.apiId && config.naverMap.apiKey) { + if (naverMap.apiId && naverMap.apiKey) { cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app)); cron.schedule("0 18 * * *", updateMinorTaxiFare(app)); } From 63d08910ade36f1902de3ef4c0318d886fa15f96 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Nov 2024 23:36:45 +0900 Subject: [PATCH 123/151] Refactor: use ts-node for local development --- nodemon.json | 17 +++++++++-------- package.json | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/nodemon.json b/nodemon.json index 5d6b9eaa..47114421 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,9 +1,10 @@ { - "ignore": ["node_modules/*"], - "watch": ["./src", ".env.development"], - "exec": "tsc && tsc-alias && node dist/index.js", - "env": { - "TZ": "Asia/Seoul", - "NODE_ENV": "development" - } -} \ No newline at end of file + "ignore": ["node_modules"], + "watch": ["src", ".env.development"], + "ext": "js,json,ts", + "exec": "ts-node --require tsconfig-paths/register src", + "env": { + "TZ": "Asia/Seoul", + "NODE_ENV": "development" + } +} diff --git a/package.json b/package.json index 95c9db46..c11b5408 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "main": "app.js", "scripts": { "preinstall": "npx only-allow pnpm", - "start": "npx tsc && tsc-alias && npx nodemon", + "start": "nodemon", "mocha": "cross-env TZ='Asia/Seoul' NODE_ENV=test mocha --require ts-node/register --require tsconfig-paths/register --recursive --reporter spec --exit", "test": "npm run sample && cross-env TZ='Asia/Seoul' npm run mocha", "build": "tsc && tsc-alias", From 6b6670b15eaf53f524c1fa2db9eac3f1dec37c22 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 19 Nov 2024 23:41:07 +0900 Subject: [PATCH 124/151] Refactor: ts conversion --- src/middlewares/ban.js | 19 ------------------- src/middlewares/ban.ts | 30 ++++++++++++++++++++++++++++++ src/modules/{ban.js => ban.ts} | 21 +++++++++++---------- 3 files changed, 41 insertions(+), 29 deletions(-) delete mode 100644 src/middlewares/ban.js create mode 100644 src/middlewares/ban.ts rename src/modules/{ban.js => ban.ts} (72%) diff --git a/src/middlewares/ban.js b/src/middlewares/ban.js deleted file mode 100644 index f70b9550..00000000 --- a/src/middlewares/ban.js +++ /dev/null @@ -1,19 +0,0 @@ -const { validateServiceBanRecord } = require("../modules/ban"); - -const serviceMapper = new Map([ - ["/rooms/create", "service"], - ["/rooms/join", "service"], -]); - -const banMiddleware = async (req, res, next) => { - const banErrorMessage = await validateServiceBanRecord( - req, - serviceMapper.get(req.originalUrl) - ); - if (banErrorMessage !== undefined) { - return res.status(400).json({ error: banErrorMessage }); - } - next(); -}; - -module.exports = banMiddleware; diff --git a/src/middlewares/ban.ts b/src/middlewares/ban.ts new file mode 100644 index 00000000..f340a093 --- /dev/null +++ b/src/middlewares/ban.ts @@ -0,0 +1,30 @@ +import type { RequestHandler } from "express"; +import { validateServiceBanRecord } from "@/modules/ban"; +import logger from "@/modules/logger"; + +const serviceMapper: Map = new Map([ + ["/rooms/create", "service"], + ["/rooms/join", "service"], +]); + +const banMiddleware: RequestHandler = async (req, res, next) => { + if (req.originalUrl === undefined) { + logger.error( + "Error occured while validateServiceBanRecord: req.originalUrl is undefined" + ); + return res.status(500).json({ + error: + "Error occured while validateServiceBanRecord: req.originalUrl is undefined", + }); + } + const banErrorMessage = await validateServiceBanRecord( + req, + serviceMapper.get(req.originalUrl) || "" + ); + if (banErrorMessage !== undefined) { + return res.status(400).json({ error: banErrorMessage }); + } + next(); +}; + +module.exports = banMiddleware; diff --git a/src/modules/ban.js b/src/modules/ban.ts similarity index 72% rename from src/modules/ban.js rename to src/modules/ban.ts index 4068db78..8bb8b473 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.ts @@ -1,3 +1,5 @@ +import type { Request } from "express"; + const logger = require("./logger"); const { banModel } = require("./stores/mongo"); @@ -5,14 +7,17 @@ const { banModel } = require("./stores/mongo"); * @param {*} req * @param {String} service */ -const validateServiceBanRecord = async (req, service) => { +export const validateServiceBanRecord = async ( + req: Request, + service: string +) => { let banRecord = undefined; try { // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 const bans = await banModel .find({ - userSid: req.session.loginInfo.sid, + userSid: req.session.loginInfo!.sid, expireAt: { $gte: req.timestamp, }, @@ -24,9 +29,7 @@ const validateServiceBanRecord = async (req, service) => { banRecord = bans[0]; } } catch (err) { - logger.error( - "Error occured while validateServiceBanRecord: " + err.message - ); + logger.error(err); return; } if (banRecord !== undefined) { @@ -34,12 +37,10 @@ const validateServiceBanRecord = async (req, service) => { .toISOString() .replace("T", " ") .split(".")[0]; - const banErrorMessage = `${req.originalUrl} : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from service until ${formattedExpireAt}.`; + const banErrorMessage = `${req.originalUrl} : user ${req.userId} (${ + req.session.loginInfo!.sid + }) is temporarily restricted from service until ${formattedExpireAt}.`; return banErrorMessage; } return; }; - -module.exports = { - validateServiceBanRecord, -}; From 68ea4b681a2d3f1ee9d31b0df77f3cde89460364 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 20 Nov 2024 00:32:00 +0900 Subject: [PATCH 125/151] Refactor: ts conversion --- ...{reportEmailPage.js => reportEmailPage.ts} | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) rename src/views/{reportEmailPage.js => reportEmailPage.ts} (87%) diff --git a/src/views/reportEmailPage.js b/src/views/reportEmailPage.ts similarity index 87% rename from src/views/reportEmailPage.js rename to src/views/reportEmailPage.ts index 629de233..d1567b81 100644 --- a/src/views/reportEmailPage.js +++ b/src/views/reportEmailPage.ts @@ -1,15 +1,28 @@ +import { ObjectId } from "mongoose"; + const emailPage = require("./emailPage").default; -const reportEmailPage = {}; +interface ReportEmailPage { + [key: string]: ( + origin: string, + name: string, + nickname: string, + roomName: string, + payer: string, + roomId: string | ObjectId + ) => string; +} + +const reportEmailPage: ReportEmailPage = {}; /* 미정산 알림 메일을 위한 템플릿 */ reportEmailPage["no-settlement"] = ( - origin, - name, - nickname, - roomName, - payer, - roomId + origin: string, + name: string, + nickname: string, + roomName: string, + payer: string, + roomId: string | ObjectId ) => emailPage( "미정산 내역 관련 안내", @@ -45,12 +58,12 @@ reportEmailPage["no-settlement"] = ( /* 미탑승 알림 메일을 위한 템플릿 */ reportEmailPage["no-show"] = ( - origin, - name, - nickname, - roomName, - payer, - roomId + origin: string, + name: string, + nickname: string, + roomName: string, + payer: string, + roomId: string | ObjectId ) => emailPage( "미탑승 내역 관련 안내", From 4fea34dc71c2a2d70a2c64047739f77fc812e913 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 26 Nov 2024 21:59:38 +0900 Subject: [PATCH 126/151] Refactor: apply new coding conventions on loadenv.ts --- src/loadenv.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/loadenv.ts b/src/loadenv.ts index c70bb4fa..4e6f649b 100644 --- a/src/loadenv.ts +++ b/src/loadenv.ts @@ -1,12 +1,12 @@ // 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옵니다. import dotenv from "dotenv"; -import { type Algorithm } from "jsonwebtoken"; -import { type ServiceAccount } from "firebase-admin"; +import type { ServiceAccount } from "firebase-admin"; +import type { Algorithm } from "jsonwebtoken"; if (process.env.NODE_ENV === undefined) { // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. // eslint-disable-next-line no-console - console.error("NODE_ENV 환경변수가 설정되어 있지 않습니다."); + console.error("There is no NODE_ENV environment variable."); process.exit(1); } dotenv.config({ path: `./.env.${process.env.NODE_ENV}` }); @@ -14,7 +14,7 @@ dotenv.config({ path: `./.env.${process.env.NODE_ENV}` }); if (process.env.DB_PATH === undefined) { // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. // eslint-disable-next-line no-console - console.error("DB_PATH 환경변수가 설정되어 있지 않습니다."); + console.error("There is no DB_PATH environment variable."); process.exit(1); } @@ -29,7 +29,7 @@ export const sparcssso = { id: process.env.SPARCSSSO_CLIENT_ID || "", // optional key: process.env.SPARCSSSO_CLIENT_KEY || "", // optional }; -export const port = process.env.PORT ? parseInt(process.env.PORT) : 80; // optional (default = 80) +export const port = parseInt(process.env.PORT || "80", 10); // optional (default = 80) export const corsWhiteList = (process.env.CORS_WHITELIST && (JSON.parse(process.env.CORS_WHITELIST) as string[])) || [true]; // optional (default = [true]) export const aws = { @@ -41,7 +41,7 @@ export const aws = { `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional }; export const jwt = { - secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", + secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", // optional option: { algorithm: "HS256" as Algorithm, // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. From c29b428b0514ce860334dff625226f93380da75c Mon Sep 17 00:00:00 2001 From: static Date: Tue, 26 Nov 2024 22:50:16 +0900 Subject: [PATCH 127/151] Refactor: convention --- src/middlewares/ban.ts | 14 ++------------ src/middlewares/index.ts | 1 + src/middlewares/responseTime.ts | 2 +- src/routes/rooms.js | 2 +- src/schedules/updateMajorTaxiFare.js | 2 +- src/schedules/updateMinorTaxiFare.js | 2 +- 6 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/middlewares/ban.ts b/src/middlewares/ban.ts index f340a093..d9a1f9d6 100644 --- a/src/middlewares/ban.ts +++ b/src/middlewares/ban.ts @@ -1,22 +1,12 @@ import type { RequestHandler } from "express"; import { validateServiceBanRecord } from "@/modules/ban"; -import logger from "@/modules/logger"; -const serviceMapper: Map = new Map([ +const serviceMapper = new Map([ ["/rooms/create", "service"], ["/rooms/join", "service"], ]); const banMiddleware: RequestHandler = async (req, res, next) => { - if (req.originalUrl === undefined) { - logger.error( - "Error occured while validateServiceBanRecord: req.originalUrl is undefined" - ); - return res.status(500).json({ - error: - "Error occured while validateServiceBanRecord: req.originalUrl is undefined", - }); - } const banErrorMessage = await validateServiceBanRecord( req, serviceMapper.get(req.originalUrl) || "" @@ -27,4 +17,4 @@ const banMiddleware: RequestHandler = async (req, res, next) => { next(); }; -module.exports = banMiddleware; +export default banMiddleware; diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index f244f41e..7484df61 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1,6 +1,7 @@ // middleware를 모아 export합니다. export { default as authMiddleware } from "./auth"; export { default as authAdminMiddleware } from "./authAdmin"; +export { default as banMiddleware } from "./ban"; export { default as corsMiddleware } from "./cors"; export { default as errorHandler } from "./errorHandler"; export { default as informationMiddleware } from "./information"; diff --git a/src/middlewares/responseTime.ts b/src/middlewares/responseTime.ts index a1bd8c54..71a8245a 100644 --- a/src/middlewares/responseTime.ts +++ b/src/middlewares/responseTime.ts @@ -1,4 +1,4 @@ -import { type Request, type Response } from "express"; +import type { Request, Response } from "express"; import responseTime from "response-time"; import logger from "@/modules/logger"; diff --git a/src/routes/rooms.js b/src/routes/rooms.js index 187243bd..f08b35eb 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -36,7 +36,7 @@ router.get( router.use(require("@/middlewares/auth").default); // 방 생성/참여전 ban 여부 확인 -router.use(require("../middlewares/ban")); +router.use(require("@/middlewares/ban").default); // 특정 id 방 세부사항 보기 router.get( diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js index 2f0afa03..68de1a3d 100644 --- a/src/schedules/updateMajorTaxiFare.js +++ b/src/schedules/updateMajorTaxiFare.js @@ -1,4 +1,4 @@ -const logger = require("../modules/logger"); +const logger = require("../modules/logger").default; const { scaledTime, updateTaxiFare } = require("../modules/fare"); diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.js index ef1dd241..5ef9d8a4 100644 --- a/src/schedules/updateMinorTaxiFare.js +++ b/src/schedules/updateMinorTaxiFare.js @@ -1,4 +1,4 @@ -const logger = require("../modules/logger"); +const logger = require("../modules/logger").default; const { updateTaxiFare } = require("../modules/fare"); From 97487fb76543bf684ec0d6502b7559f74503770b Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 26 Nov 2024 22:58:15 +0900 Subject: [PATCH 128/151] Refactor: import convention --- src/modules/ban.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/ban.ts b/src/modules/ban.ts index 8bb8b473..2a404f27 100644 --- a/src/modules/ban.ts +++ b/src/modules/ban.ts @@ -1,7 +1,7 @@ import type { Request } from "express"; -const logger = require("./logger"); -const { banModel } = require("./stores/mongo"); +import logger from "@/modules/logger"; +import { banModel } from "./stores/mongo"; /** * @param {*} req From b14e48ea8b65bdd0713b4f9a88e91e752ae99197 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 26 Nov 2024 23:28:53 +0900 Subject: [PATCH 129/151] Refactor: convention 2 --- src/modules/auths/login.ts | 2 +- src/modules/ban.ts | 5 --- src/modules/fcm.ts | 72 ++++++++++++++++---------------- src/modules/populates/chats.ts | 4 +- src/modules/populates/reports.ts | 2 +- src/modules/populates/rooms.ts | 31 +++++++------- src/modules/slackNotification.ts | 2 +- src/routes/index.ts | 2 +- src/schedules/index.ts | 5 ++- 9 files changed, 60 insertions(+), 65 deletions(-) diff --git a/src/modules/auths/login.ts b/src/modules/auths/login.ts index a55677b6..962c260f 100644 --- a/src/modules/auths/login.ts +++ b/src/modules/auths/login.ts @@ -1,4 +1,4 @@ -import { type Request } from "express"; +import type { Request } from "express"; import { session as sessionConfig } from "@/loadenv"; import logger from "@/modules/logger"; diff --git a/src/modules/ban.ts b/src/modules/ban.ts index 2a404f27..366e5c54 100644 --- a/src/modules/ban.ts +++ b/src/modules/ban.ts @@ -1,12 +1,7 @@ import type { Request } from "express"; - import logger from "@/modules/logger"; import { banModel } from "./stores/mongo"; -/** - * @param {*} req - * @param {String} service - */ export const validateServiceBanRecord = async ( req: Request, service: string diff --git a/src/modules/fcm.ts b/src/modules/fcm.ts index ae5abf5b..50ee26db 100644 --- a/src/modules/fcm.ts +++ b/src/modules/fcm.ts @@ -7,7 +7,7 @@ import { notificationOptionModel, topicSubscriptionModel, } from "@/modules/stores/mongo"; -import { type ChatType } from "@/types/mongo"; +import type { ChatType } from "@/types/mongo"; /** * credential을 등록합니다. @@ -26,14 +26,14 @@ export const initializeApp = () => { /** * 사용자의 ObjectId와 FCM device token이 주어졌을 때, 해당 deviceToken을 사용자의 토큰으로 DB에 등록합니다. - * @param {string} userId - 사용자의 ObjectId입니다. - * @param {string} deviceToken - 등록하려는 FCM device token입니다. - * @return {Promise>} 변경된 사용자의 deviceToken의 목록 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. + * @param userId - 사용자의 ObjectId입니다. + * @param deviceToken - 등록하려는 FCM device token입니다. + * @return 변경된 사용자의 deviceToken의 목록 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ export const registerDeviceToken = async ( userId: string, deviceToken: string -): Promise => { +) => { try { // 디바이스 토큰을 다른 사용자가 사용하고 있는지 확인 및 삭제합니다. await deviceTokenModel.updateMany( @@ -67,8 +67,8 @@ export const registerDeviceToken = async ( /** * 사용자의 ObjectId와 FCM device token이 주어졌을 때, 해당 사용자의 해당 deviceToken을 DB에서 삭제합니다. - * @param {string} deviceToken - 삭제하려는 FCM device token입니다. - * @return {Promise} 해당 deviceToken을 가진 모든 사용자로부터 해당 deviceToken을 삭제하는 데 성공하면 true, 하나 이상의 사용자에게서 해당 deviceToken을 삭제하는 데 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. + * @param deviceToken - 삭제하려는 FCM device token입니다. + * @return 해당 deviceToken을 가진 모든 사용자로부터 해당 deviceToken을 삭제하는 데 성공하면 true, 하나 이상의 사용자에게서 해당 deviceToken을 삭제하는 데 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. */ export const unregisterDeviceToken = async (deviceToken: string) => { try { @@ -95,9 +95,9 @@ export const unregisterDeviceToken = async (deviceToken: string) => { /** * 메시지 전송에 실패한 deviceToken을 DB에서 삭제합니다. - * @param {Array} deviceTokens - 사용자의 ObjectId입니다. - * @param {Array} fcmResponses - 등록하려는 FCM device token입니다. - * @return {Promise>} 각각의 토큰들의 삭제 성공 여부가 저장된 Array를 반환합니다. 해당 토큰을 DB에서 삭제하는 데 성공했으면 true, 아니면 false가 포함됩니다. + * @param deviceTokens - 사용자의 ObjectId입니다. + * @param fcmResponses - 등록하려는 FCM device token입니다. + * @return 각각의 토큰들의 삭제 성공 여부가 저장된 Array를 반환합니다. 해당 토큰을 DB에서 삭제하는 데 성공했으면 true, 아니면 false가 포함됩니다. */ const removeExpiredTokens = async ( deviceTokens: string[], @@ -131,8 +131,8 @@ const removeExpiredTokens = async ( /** * 사용자의 FCM device token이 현재 사용 가능한지 검증합니다. * @summary 해당 디바이스에 dry-run 방식으로 메시지 전송을 시험함으로써 해당 deviceToken이 사용 가능한지 검증합니다. dry-run 시 FCM 서버에는 메시지 전송 요청이 전송되지만, 실제 기기에는 알림이 전송되지 않습니다. - * @param {string} deviceToken - 사용 가능 여부를 확인하려고 하는 FCM device token입니다. - * @return {Promise} 해당 디바이스에 알림을 보낸다는 요청을 FCM 서버에 성공적으로 보냈으면 true, 아니면 false를 반환합니다. + * @param deviceToken - 사용 가능 여부를 확인하려고 하는 FCM device token입니다. + * @return 해당 디바이스에 알림을 보낸다는 요청을 FCM 서버에 성공적으로 보냈으면 true, 아니면 false를 반환합니다. */ export const validateDeviceToken = async (deviceToken: string) => { try { @@ -152,14 +152,14 @@ export const validateDeviceToken = async (deviceToken: string) => { /** * 사용자들의 ObjectId의 배열이 주어졌을 때, 해당 사용자들의 모든 deviceToken을 하나의 Array로 반환합니다. - * @param {Array} userIds - 사용자의 ObjectId로 이루어진 Array입니다. - * @param {Object?} notificationOptions - 특정 알림 설정을 비활성화한 사용자를 필터링하기 위해 사용되는 Object입니다. + * @param userIds - 사용자의 ObjectId로 이루어진 Array입니다. + * @param notificationOptions - 특정 알림 설정을 비활성화한 사용자를 필터링하기 위해 사용되는 Object입니다. * @param {Boolean?} notificationOptions.chatting - true 또는 false로 주어진 경우, 채팅 알림 설정이 각각 true 또는 false로 설정된 사용자들의 deviceToken만 반환합니다. - * @return {Promise>} deviceToken의 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. + * @return deviceToken의 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ export const getTokensOfUsers = async ( userIds: string[], - notificationOptions: Object = {} + notificationOptions: object = {} ) => { const deviceTokensOfUsers = ( await Promise.all( @@ -187,13 +187,13 @@ export const getTokensOfUsers = async ( /** * 주어진 token들에 메시지 알림을 전송합니다. * TODO: 알림 전송 실패한 토큰 삭제하기 - * @param {Array} tokens - 메시지 알림을 받을 기기의 deviceToken들로 구성된 Array입니다. - * @param {ChatType} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. - * @param {string} title - 보낼 메시지의 제목입니다. - * @param {string} body - 보낼 메시지의 본문입니다. - * @param {string?} icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. - * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. - * @return {Promise} 메시지 알림 전송에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. + * @param tokens - 메시지 알림을 받을 기기의 deviceToken들로 구성된 Array입니다. + * @param type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. + * @param title - 보낼 메시지의 제목입니다. + * @param body - 보낼 메시지의 본문입니다. + * @param icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. + * @param link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. + * @return 메시지 알림 전송에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ export const sendMessageByTokens = async ( tokens: string[], @@ -237,13 +237,13 @@ export const sendMessageByTokens = async ( /** * 주어진 topic을 구독하고 있는 모든 기기에 메시지 알림을 전송합니다. - * @param {string} topic - 메시지 알림을 보낼 기기들이 구독하고 있는 topic입니다. - * @param {ChatType} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. - * @param {string} title - 보낼 메시지의 제목입니다. - * @param {string} body - 보낼 메시지의 본문입니다. - * @param {string?} icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. - * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. - * @return {Promise} 메시지 알림 전송에 성공했으면 true, 아니면 false를 반환합니다. + * @param topic - 메시지 알림을 보낼 기기들이 구독하고 있는 topic입니다. + * @param type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. + * @param title - 보낼 메시지의 제목입니다. + * @param body - 보낼 메시지의 본문입니다. + * @param icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. + * @param link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. + * @return 메시지 알림 전송에 성공했으면 true, 아니면 false를 반환합니다. */ export const sendMessageByTopic = async ( topic: string, @@ -278,9 +278,9 @@ export const sendMessageByTopic = async ( /** * 주어진 사용자를 특정한 topic에 구독시킵니다. - * @param {string} userId - topic을 구독할 사용자의 ObjectId입니다. - * @param {string} topic - 구독할 topic입니다. - * @return {Promise} 토픽 구독에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. + * @param userId - topic을 구독할 사용자의 ObjectId입니다. + * @param topic - 구독할 topic입니다. + * @return 토픽 구독에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ export const subscribeUserToTopic = async (userId: string, topic: string) => { try { @@ -329,9 +329,9 @@ export const subscribeUserToTopic = async (userId: string, topic: string) => { /** * 주어진 사용자를 특정한 topic으로부터 구독 해제시킵니다. - * @param {string} userId - topic을 구독 해제할 사용자의 id입니다. - * @param {string} topic - 구독을 해제할 topic입니다. - * @return {Promise} 토픽 구독 해제에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. + * @param userId - topic을 구독 해제할 사용자의 id입니다. + * @param topic - 구독을 해제할 topic입니다. + * @return 토픽 구독 해제에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ export const unsubscribeUserFromTopic = async ( userId: string, diff --git a/src/modules/populates/chats.ts b/src/modules/populates/chats.ts index 633dd174..077f7dad 100644 --- a/src/modules/populates/chats.ts +++ b/src/modules/populates/chats.ts @@ -1,6 +1,6 @@ -import { type User, type Chat } from "@/types/mongo"; +import type { User, Chat } from "@/types/mongo"; -/** @constant {{path: string, select: string}[]} +/** * 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다. */ export const chatPopulateOption = [ diff --git a/src/modules/populates/reports.ts b/src/modules/populates/reports.ts index a9fe3c9e..6fcf60f3 100644 --- a/src/modules/populates/reports.ts +++ b/src/modules/populates/reports.ts @@ -1,4 +1,4 @@ -import { type User, type Report } from "@/types/mongo"; +import type { User, Report } from "@/types/mongo"; export const reportPopulateOption = [ { diff --git a/src/modules/populates/rooms.ts b/src/modules/populates/rooms.ts index 15f39338..6eb87461 100644 --- a/src/modules/populates/rooms.ts +++ b/src/modules/populates/rooms.ts @@ -1,14 +1,13 @@ -import { - type User, - type SettlementStatus, - type Participant, - type Room, - type Location, +import type { + User, + SettlementStatus, + Participant, + Room, + Location, } from "@/types/mongo"; /** * 쿼리를 통해 얻은 Room Document를 populate할 설정값을 정의합니다. - * @constant {{path: string, select: string, populate?: {path: string, select: string}}[]} */ export const roomPopulateOption = [ { path: "from", select: "_id koName enName" }, @@ -49,12 +48,12 @@ export interface FormattedRoom /** * Room Object가 주어졌을 때 room의 part array의 각 요소를 API 명세에서와 같이 {userId: String, ... , isSettlement: String}으로 가공합니다. * 또한, 방이 현재 출발했는지 유무인 isDeparted 속성을 추가합니다. - * @param {PopulatedRoom} roomObject - 정산 정보를 가공할 room Object로, Mongoose Document가 아닌 순수 Javascript Object여야 합니다. - * @param {Object} options - 추가 파라미터로, 기본값은 {}입니다. - * @param {Boolean} options.includeSettlement - 반환 결과에 정산 정보를 포함할 지 여부로, 기본값은 true입니다. - * @param {Date} options.timestamp - 방의 출발 여부(isDeparted)를 판단하는 기준이 되는 시각입니다. - * @param {Boolean} options.isOver - 방의 완료 여부(isOver)로, 기본값은 false입니다. includeSettlement가 false인 경우 roomDocument의 isOver 속성은 undefined로 설정됩니다. - * @return {FormattedRoom} 정산 여부가 위와 같이 가공되고 isDeparted 속성이 추가된 Room Object가 반환됩니다. + * @param roomObject - 정산 정보를 가공할 room Object로, Mongoose Document가 아닌 순수 Javascript Object여야 합니다. + * @param options - 추가 파라미터로, 기본값은 {}입니다. + * @param options.includeSettlement - 반환 결과에 정산 정보를 포함할 지 여부로, 기본값은 true입니다. + * @param options.timestamp - 방의 출발 여부(isDeparted)를 판단하는 기준이 되는 시각입니다. + * @param options.isOver - 방의 완료 여부(isOver)로, 기본값은 false입니다. includeSettlement가 false인 경우 roomDocument의 isOver 속성은 undefined로 설정됩니다. + * @return 정산 여부가 위와 같이 가공되고 isDeparted 속성이 추가된 Room Object가 반환됩니다. */ export const formatSettlement = ( roomObject: PopulatedRoom, @@ -83,9 +82,9 @@ export const formatSettlement = ( /** * roomPopulateOption을 사용해 populate된 Room Object와 사용자의 id(userId)가 주어졌을 때, 해당 사용자의 정산 상태를 반환합니다. - * @param {PopulatedRoom} roomObject - roomPopulateOption을 사용해 populate된 변환한 Room Object입니다. - * @param {String} userId - 방 완료 상태를 확인하려는 사용자의 id(user.id)입니다. - * @return {Boolean | undefined} 사용자의 해당 방에 대한 완료 여부(true | false)를 반환합니다. 사용자가 참여중인 방이 아닐 경우 undefined를 반환합니다. + * @param roomObject - roomPopulateOption을 사용해 populate된 변환한 Room Object입니다. + * @param userId - 방 완료 상태를 확인하려는 사용자의 id(user.id)입니다. + * @return 사용자의 해당 방에 대한 완료 여부(true | false)를 반환합니다. 사용자가 참여중인 방이 아닐 경우 undefined를 반환합니다. **/ export const getIsOver = (roomObject: PopulatedRoom, userId: string) => { // room document의 part subdoocument에서 사용자 id와 일치하는 정산 정보를 찾습니다. diff --git a/src/modules/slackNotification.ts b/src/modules/slackNotification.ts index bd3b9837..c70cfdb6 100644 --- a/src/modules/slackNotification.ts +++ b/src/modules/slackNotification.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { nodeEnv, slackWebhookUrl as slackUrl } from "@/loadenv"; import logger from "@/modules/logger"; -import { type Report } from "@/types/mongo"; +import type { Report } from "@/types/mongo"; export const sendTextToReportChannel = (text: string) => { if (!slackUrl.report) return; diff --git a/src/routes/index.ts b/src/routes/index.ts index ddbe083a..83bc81dc 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,10 +2,10 @@ export { default as adminRouter } from "./admin"; export { default as authRouter } from "./auth"; export { default as chatRouter } from "./chats"; export { default as docsRouter } from "./docs"; +export { default as fareRouter } from "./fare"; export { default as locationRouter } from "./locations"; export { default as logininfoRouter } from "./logininfo"; export { default as notificationRouter } from "./notifications"; export { default as reportRouter } from "./reports"; export { default as roomRouter } from "./rooms"; export { default as userRouter } from "./users"; -export { default as fareRouter } from "./fare"; diff --git a/src/schedules/index.ts b/src/schedules/index.ts index 1908fadd..f714dd61 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -1,14 +1,15 @@ -import { type Express } from "express"; +import type { Express } from "express"; import cron from "node-cron"; +import { naverMap } from "@/loadenv"; import notifyBeforeDepart from "./notifyBeforeDepart"; import notifyAfterArrival from "./notifyAfterArrival"; import updateMajorTaxiFare from "./updateMajorTaxiFare"; import updateMinorTaxiFare from "./updateMinorTaxiFare"; -import { naverMap } from "@/loadenv"; const registerSchedules = (app: Express) => { cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); cron.schedule("*/10 * * * *", notifyAfterArrival(app)); + if (naverMap.apiId && naverMap.apiKey) { cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app)); cron.schedule("0 18 * * *", updateMinorTaxiFare(app)); From 1c396a21480683baf7e5063595a43e6406e63b24 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 26 Nov 2024 23:56:33 +0900 Subject: [PATCH 130/151] Refactor: convention 3 --- src/routes/users.ts | 10 ++-- src/services/users.ts | 103 +++++++++++++++++++++++------------------- 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/src/routes/users.ts b/src/routes/users.ts index 1f6cb288..45514925 100755 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -1,6 +1,6 @@ import express from "express"; import { body } from "express-validator"; -import validator from "@/middlewares/validator"; +import { authMiddleware, validatorMiddleware } from "@/middlewares"; import patterns from "@/modules/patterns"; const router = express.Router(); @@ -9,7 +9,7 @@ import * as userHandlers from "@/services/users"; import { replaceSpaceInNickname } from "@/modules/modifyProfile"; // 라우터 접근 시 로그인 필요 -router.use(require("@/middlewares/auth").default); +router.use(authMiddleware); // 이용 약관에 동의합니다. router.post( @@ -27,7 +27,7 @@ router.post( body("nickname") .customSanitizer(replaceSpaceInNickname) .matches(patterns.user.nickname), - validator, + validatorMiddleware, userHandlers.editNicknameHandler ); @@ -38,7 +38,7 @@ router.get("/resetNickname", userHandlers.resetNicknameHandler); router.post( "/editAccount", body("account").matches(patterns.user.account), - validator, + validatorMiddleware, userHandlers.editAccountHandler ); @@ -46,7 +46,7 @@ router.post( router.post( "/editProfileImg/getPUrl", body("type").matches(patterns.user.profileImgType), - validator, + validatorMiddleware, userHandlers.editProfileImgGetPUrlHandler ); diff --git a/src/services/users.ts b/src/services/users.ts index 2fc8f0ba..a51ac859 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,40 +1,44 @@ -import type { Request, Response } from "express"; +import type { RequestHandler } from "express"; import { userModel, banModel } from "@/modules/stores/mongo"; import logger from "@/modules/logger"; -import * as aws from "@/modules/stores/aws"; - import { generateNickname, generateProfileImageUrl, } from "@/modules/modifyProfile"; +import * as aws from "@/modules/stores/aws"; + // 이벤트 코드입니다. // const { contracts } = require("@/lottery"); -export const agreeOnTermsOfServiceHandler = async ( - req: Request, - res: Response +export const agreeOnTermsOfServiceHandler: RequestHandler = async ( + req, + res ) => { try { let user = await userModel.findOne({ id: req.userId }); if (user && user.agreeOnTermsOfService !== true) { user.agreeOnTermsOfService = true; await user.save(); - res + return res .status(200) .send( "Users/agreeOnTermsOfService : agree on Terms of Service successful" ); } else { - res.status(400).send("Users/agreeOnTermsOfService : already agreed"); + return res + .status(400) + .send("Users/agreeOnTermsOfService : already agreed"); } } catch { - res.status(500).send("Users/agreeOnTermsOfService : internal server error"); + return res + .status(500) + .send("Users/agreeOnTermsOfService : internal server error"); } }; -export const getAgreeOnTermsOfServiceHandler = async ( - req: Request, - res: Response +export const getAgreeOnTermsOfServiceHandler: RequestHandler = async ( + req, + res ) => { try { const user = await userModel @@ -42,18 +46,18 @@ export const getAgreeOnTermsOfServiceHandler = async ( .lean(); if (user) { const agreeOnTermsOfService = user.agreeOnTermsOfService === true; - res.json({ agreeOnTermsOfService }); + return res.json({ agreeOnTermsOfService }); } } catch { - res + return res .status(500) .send("Users/getAgreeOnTermsOfService : internal server error"); } }; -export const editNicknameHandler = async (req: Request, res: Response) => { +export const editNicknameHandler: RequestHandler = async (req, res) => { try { - const newNickname = req.body.nickname; + const newNickname = req.body.nickname; // TODO: Typing const result = await userModel.findOneAndUpdate( { id: req.userId }, { nickname: newNickname } @@ -66,21 +70,23 @@ export const editNicknameHandler = async (req: Request, res: Response) => { // req.timestamp // ); - res + return res .status(200) .send("Users/editNickname : edit user nickname successful"); } else { - res.status(400).send("Users/editNickname : such user id does not exist"); + return res + .status(400) + .send("Users/editNickname : such user id does not exist"); } } catch (err) { logger.error(err); - res.status(500).send("Users/editNickname : internal server error"); + return res.status(500).send("Users/editNickname : internal server error"); } }; -export const editAccountHandler = async (req: Request, res: Response) => { +export const editAccountHandler: RequestHandler = async (req, res) => { try { - const newAccount = req.body.account; + const newAccount = req.body.account; // TODO: Typing const result = await userModel.findOneAndUpdate( { id: req.userId }, { account: newAccount } @@ -94,22 +100,26 @@ export const editAccountHandler = async (req: Request, res: Response) => { // newAccount // ); - res.status(200).send("Users/editAccount : edit user account successful"); + return res + .status(200) + .send("Users/editAccount : edit user account successful"); } else { - res.status(400).send("Users/editAccount : such user id does not exist"); + return res + .status(400) + .send("Users/editAccount : such user id does not exist"); } } catch (err) { logger.error(err); - res.status(500).send("Users/editAccount : internal server error"); + return res.status(500).send("Users/editAccount : internal server error"); } }; -export const editProfileImgGetPUrlHandler = async ( - req: Request, - res: Response +export const editProfileImgGetPUrlHandler: RequestHandler = async ( + req, + res ) => { try { - const type = req.body.type; + const type = req.body.type; // TODO: Typing const user = await userModel.findOne({ id: req.userId }, "_id"); if (!user) { return res @@ -125,22 +135,19 @@ export const editProfileImgGetPUrlHandler = async ( } data.fields["Content-Type"] = type; data.fields["key"] = key; - res.json({ + return res.json({ url: data.url, fields: data.fields, }); }); } catch (e) { - res + return res .status(500) .send("Users/editProfileImg/getPUrl : internal server error"); } }; -export const editProfileImgDoneHandler = async ( - req: Request, - res: Response -) => { +export const editProfileImgDoneHandler: RequestHandler = async (req, res) => { try { const user = await userModel.findOne({ id: req.userId }, "_id"); if (!user) { @@ -166,37 +173,39 @@ export const editProfileImgDoneHandler = async ( .status(500) .send("Users/editProfileImg/done : internal server error"); } - res.json({ + return res.json({ result: true, profileImageUrl: userAfter.profileImageUrl, }); }); } catch (e) { - res.status(500).send("Users/editProfileImg/done : internal server error"); + return res + .status(500) + .send("Users/editProfileImg/done : internal server error"); } }; -export const resetNicknameHandler = async (req: Request, res: Response) => { +export const resetNicknameHandler: RequestHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( { id: req.userId }, - { nickname: generateNickname(req.body.id) }, + { nickname: generateNickname(req.body.id) }, // TODO: Typing or Validation { new: true } ); if (!result) return res .status(400) .send("Users/resetNickname : such user does not exist"); - res + return res .status(200) .send("Users/resetNickname : reset user nickname successful"); } catch (err) { logger.error(err); - res.status(500).send("Users/resetNickname : internal server error"); + return res.status(500).send("Users/resetNickname : internal server error"); } }; -export const resetProfileImgHandler = async (req: Request, res: Response) => { +export const resetProfileImgHandler: RequestHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( { id: req.userId }, @@ -207,15 +216,17 @@ export const resetProfileImgHandler = async (req: Request, res: Response) => { return res .status(400) .send("Users/resetProfileImg : such user does not exist"); - res + return res .status(200) .send("Users/resetProfileImg : reset user profile image successful"); } catch (err) { - res.status(500).send("Users/resetProfileImg : internal server error"); + return res + .status(500) + .send("Users/resetProfileImg : internal server error"); } }; -export const getBanRecordHandler = async (req: Request, res: Response) => { +export const getBanRecordHandler: RequestHandler = async (req, res) => { try { // 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 const result = await banModel @@ -225,8 +236,8 @@ export const getBanRecordHandler = async (req: Request, res: Response) => { .sort({ expireAt: -1 }); if (!result) return res.status(500).send("Users/getBanRecord : internal server error"); - res.status(200).json(result); + return res.status(200).json(result); } catch (err) { - res.status(500).send("Users/getBanRecord : internal server error"); + return res.status(500).send("Users/getBanRecord : internal server error"); } }; From 327ba686dfe3264249767c44f8b0407b92c198be Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 27 Nov 2024 00:19:10 +0900 Subject: [PATCH 131/151] Refactor: esm convention --- src/services/reports.js | 2 +- src/views/reportEmailPage.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/services/reports.js b/src/services/reports.js index afbc08ba..f4cb17f4 100644 --- a/src/services/reports.js +++ b/src/services/reports.js @@ -2,7 +2,7 @@ const { userModel, reportModel, roomModel } = require("@/modules/stores/mongo"); const { reportPopulateOption } = require("@/modules/populates/reports"); const { sendReportEmail } = require("@/modules/email"); const logger = require("@/modules/logger").default; -const reportEmailPage = require("@/views/reportEmailPage"); +const reportEmailPage = require("@/views/reportEmailPage").default; const { notifyReportToReportChannel } = require("@/modules/slackNotification"); const createHandler = async (req, res) => { diff --git a/src/views/reportEmailPage.ts b/src/views/reportEmailPage.ts index d1567b81..779f36e0 100644 --- a/src/views/reportEmailPage.ts +++ b/src/views/reportEmailPage.ts @@ -1,6 +1,5 @@ -import { ObjectId } from "mongoose"; - -const emailPage = require("./emailPage").default; +import type { ObjectId } from "mongoose"; +import emailPage from "./emailPage"; interface ReportEmailPage { [key: string]: ( @@ -96,4 +95,4 @@ reportEmailPage["no-show"] = ( ` ); -module.exports = reportEmailPage; +export default reportEmailPage; From 2f60fd978f3dd22ed602d9bfd2b187ecccf64873 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 00:20:11 +0900 Subject: [PATCH 132/151] Refactor: tsconfig.json --- tsconfig.json | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 09d4273e..e7575db4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,23 @@ { - "compilerOptions": { - "target": "es6", - "module": "node16", - "moduleResolution": "node16", - "allowJs": true, - "outDir": "./dist", - "resolveJsonModule": true, - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": "./src", - "paths": { - "@/*": ["./*"] - } - }, - "include": ["src"], - "exclude": ["dist", "node_modules"], - "ts-node": { - "files": true + "compilerOptions": { + "target": "es6", + "module": "node16", + "moduleResolution": "node16", + "allowJs": true, + "outDir": "./dist", + "resolveJsonModule": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "./src", + "paths": { + "@/*": ["./*"] } + }, + "include": ["src"], + "exclude": ["dist", "node_modules"], + "ts-node": { + "files": true } - \ No newline at end of file +} From b3b17b5da09e7e3a0ac95701a8378587e4fb9b1e Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 00:41:31 +0900 Subject: [PATCH 133/151] Refactor: convention 4 --- src/index.ts | 43 ++++++++++++++++++------------------ src/views/reportEmailPage.ts | 24 ++++++++++---------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/index.ts b/src/index.ts index f741d146..ced953e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,42 +6,43 @@ import http from "http"; import { nodeEnv, mongo as mongoUrl, port as httpPort } from "@/loadenv"; import { corsMiddleware, - sessionMiddleware, + errorHandler, informationMiddleware, - responseTimeMiddleware, limitRateMiddleware, originValidatorMiddleware, - errorHandler, + responseTimeMiddleware, + sessionMiddleware, } from "@/middlewares"; import { + adminRouter, authRouter, - logininfoRouter, - userRouter, - roomRouter, chatRouter, - locationRouter, - reportRouter, - notificationRouter, - adminRouter, docsRouter, fareRouter, + locationRouter, + logininfoRouter, + notificationRouter, + reportRouter, + roomRouter, + userRouter, } from "@/routes"; -import { initializeApp } from "@/modules/fcm"; + +import { initializeApp as initializeFirebase } from "@/modules/fcm"; import { initializeDatabase as initializeFareDatabase } from "@/modules/fare"; import logger from "@/modules/logger"; -import { connectDatabase } from "@/modules/stores/mongo"; import { startSocketServer } from "@/modules/socket"; +import { connectDatabase } from "@/modules/stores/mongo"; import registerSchedules from "@/schedules"; // Firebase Admin 초기설정 -initializeApp(); - -// 익스프레스 서버 생성 -const app = express(); +initializeFirebase(); // 데이터베이스 연결 connectDatabase(mongoUrl); +// 익스프레스 서버 생성 +const app = express(); + // [Middleware] request body 파싱 app.use(express.urlencoded({ extended: false })); app.use(express.json()); @@ -80,14 +81,14 @@ app.use(originValidatorMiddleware); // [Router] APIs app.use("/auth", authRouter); -app.use("/logininfo", logininfoRouter); -app.use("/users", userRouter); -app.use("/rooms", roomRouter); app.use("/chats", chatRouter); +app.use("/fare", fareRouter); app.use("/locations", locationRouter); -app.use("/reports", reportRouter); +app.use("/logininfo", logininfoRouter); app.use("/notifications", notificationRouter); -app.use("/fare", fareRouter); +app.use("/reports", reportRouter); +app.use("/rooms", roomRouter); +app.use("/users", userRouter); // [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. app.use(errorHandler); diff --git a/src/views/reportEmailPage.ts b/src/views/reportEmailPage.ts index 779f36e0..49691aad 100644 --- a/src/views/reportEmailPage.ts +++ b/src/views/reportEmailPage.ts @@ -16,12 +16,12 @@ const reportEmailPage: ReportEmailPage = {}; /* 미정산 알림 메일을 위한 템플릿 */ reportEmailPage["no-settlement"] = ( - origin: string, - name: string, - nickname: string, - roomName: string, - payer: string, - roomId: string | ObjectId + origin, + name, + nickname, + roomName, + payer, + roomId ) => emailPage( "미정산 내역 관련 안내", @@ -57,12 +57,12 @@ reportEmailPage["no-settlement"] = ( /* 미탑승 알림 메일을 위한 템플릿 */ reportEmailPage["no-show"] = ( - origin: string, - name: string, - nickname: string, - roomName: string, - payer: string, - roomId: string | ObjectId + origin, + name, + nickname, + roomName, + payer, + roomId ) => emailPage( "미탑승 내역 관련 안내", From 99430cd623e9cdbe77d935893bac592029afbdac Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 27 Nov 2024 00:43:01 +0900 Subject: [PATCH 134/151] Refactor: @ convention --- src/modules/ban.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/ban.ts b/src/modules/ban.ts index 366e5c54..f5276f23 100644 --- a/src/modules/ban.ts +++ b/src/modules/ban.ts @@ -1,6 +1,6 @@ import type { Request } from "express"; import logger from "@/modules/logger"; -import { banModel } from "./stores/mongo"; +import { banModel } from "@/modules/stores/mongo"; export const validateServiceBanRecord = async ( req: Request, From e93d89b7ac26c06d624ac2a243ff736035157601 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 01:21:33 +0900 Subject: [PATCH 135/151] Fix: redis undefined error --- src/modules/stores/sessionStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/stores/sessionStore.ts b/src/modules/stores/sessionStore.ts index 11ec9a78..2eed4e2e 100644 --- a/src/modules/stores/sessionStore.ts +++ b/src/modules/stores/sessionStore.ts @@ -1,6 +1,6 @@ import MongoStore from "connect-mongo"; import RedisStore from "connect-redis"; -import redis from "redis"; +import { createClient } from "redis"; import { redis as redisUrl, mongo as mongoUrl, @@ -11,7 +11,7 @@ import logger from "@/modules/logger"; const getSessionStore = () => { // 환경변수 REDIS_PATH 유무에 따라 session 저장 방식이 변경됩니다. if (redisUrl) { - const client = redis.createClient({ + const client = createClient({ url: redisUrl, }); From 84a732e8b1566833b4fa1c6a84dbfa344279670f Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 01:27:29 +0900 Subject: [PATCH 136/151] Fix: jwt undefiend error --- src/services/auth.js | 2 +- src/services/auth.mobile.js | 2 +- src/services/auth.replace.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/auth.js b/src/services/auth.js index cd660ab5..2b9ba658 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -9,7 +9,7 @@ const { generateProfileImageUrl, getFullUsername, } = require("@/modules/modifyProfile"); -const jwt = require("@/modules/auths/jwt").default; +const jwt = require("@/modules/auths/jwt"); const logger = require("@/modules/logger").default; // SPARCS SSO diff --git a/src/services/auth.mobile.js b/src/services/auth.mobile.js index 8e66db0e..6ae14db2 100644 --- a/src/services/auth.mobile.js +++ b/src/services/auth.mobile.js @@ -2,7 +2,7 @@ const { userModel } = require("@/modules/stores/mongo"); const { login } = require("@/modules/auths/login"); const { registerDeviceToken, unregisterDeviceToken } = require("@/modules/fcm"); -const jwt = require("@/modules/auths/jwt").default; +const jwt = require("@/modules/auths/jwt"); const logger = require("@/modules/logger").default; const { TOKEN_EXPIRED, TOKEN_INVALID } = require("@/loadenv").jwt; diff --git a/src/services/auth.replace.js b/src/services/auth.replace.js index 65ae92c7..111c7077 100644 --- a/src/services/auth.replace.js +++ b/src/services/auth.replace.js @@ -7,7 +7,7 @@ const { generateProfileImageUrl, } = require("@/modules/modifyProfile"); const logger = require("@/modules/logger").default; -const jwt = require("@/modules/auths/jwt").default; +const jwt = require("@/modules/auths/jwt"); const { registerDeviceTokenHandler, tryLogin } = require("@/services/auth"); const loginReplacePage = require("@/views/loginReplacePage").default; From e0e99c604e6987506990a9069a538cddfd13a139 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 01:58:46 +0900 Subject: [PATCH 137/151] Fix: minor bugs --- src/modules/stores/mongo.ts | 2 +- src/services/chats.js | 2 +- src/types/mongo.d.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index b4affe32..2c2e17e6 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -155,7 +155,7 @@ const roomSchema = new Schema({ }, // 참여 멤버 및 정산 여부 madeat: { type: Date, required: true }, // 생성 날짜 settlementTotal: { type: Number, default: 0, required: true }, - maxPartLength: { type: Number, require: true, default: 4 }, + maxPartLength: { type: Number, required: true, default: 4 }, }); export const roomModel = model("Room", roomSchema); diff --git a/src/services/chats.js b/src/services/chats.js index f8190cfd..b6bd6467 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -1,7 +1,7 @@ const { chatModel, userModel, roomModel } = require("@/modules/stores/mongo"); const { chatPopulateOption } = require("@/modules/populates/chats"); const { roomPopulateOption } = require("@/modules/populates/rooms"); -const aws = require("@/modules/stores/aws").default; +const aws = require("@/modules/stores/aws"); const { transformChatsForRoom, emitChatEvent, diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index 93fcb382..718b342e 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -1,4 +1,4 @@ -import { Document, Types } from "mongoose"; +import type { Document, Types } from "mongoose"; export interface User extends Document { /** 사용자의 실명. */ @@ -13,6 +13,7 @@ export interface User extends Document { ongoingRoom?: Types.Array; /** 사용자가 참여한 방 중 완료된 방의 배열. */ doneRoom?: Types.Array; + /** 계정 탈퇴 여부. */ withdraw: boolean; /** 사용자의 전화번호. 2023 가을 이벤트부터 추가됨. */ phoneNumber?: string; From d8c691960feb234d21a80215487c33ce47bf4285 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 27 Nov 2024 02:08:37 +0900 Subject: [PATCH 138/151] Refactor: type --- src/modules/stores/mongo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index b4affe32..a884b455 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -1,4 +1,4 @@ -import mongoose, { model, Schema, Types } from "mongoose"; +import mongoose, { model, Schema, type Types } from "mongoose"; import logger from "@/modules/logger"; import type { User, From 8bf159903951b4d9fec86b1a416ae41fc3a40024 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 27 Nov 2024 02:26:29 +0900 Subject: [PATCH 139/151] Refactor: es6 --- src/modules/stores/mongo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index 25b3d1f6..76924f2a 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -263,7 +263,7 @@ database.on("error", function (err) { }); export const connectDatabase = (mongoUrl: string) => { - database.on("disconnected", function () { + database.on("disconnected", () => { // 데이터베이스 연결이 끊어지면 5초 후 재연결을 시도합니다. logger.error("Disconnected from database!"); setTimeout(() => { From 5172ea55b031a015c648e41fd4e7d965ceeb4cbf Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 02:52:51 +0900 Subject: [PATCH 140/151] Fix: scripts --- package.json | 6 +++--- scripts/chatPaymentSettlementUpdater.js | 2 +- scripts/profileImageUrlUpdater.js | 2 +- tsconfig.build.json | 4 ++++ tsconfig.json | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 tsconfig.build.json diff --git a/package.json b/package.json index c11b5408..2882496f 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,12 @@ "preinstall": "npx only-allow pnpm", "start": "nodemon", "mocha": "cross-env TZ='Asia/Seoul' NODE_ENV=test mocha --require ts-node/register --require tsconfig-paths/register --recursive --reporter spec --exit", - "test": "npm run sample && cross-env TZ='Asia/Seoul' npm run mocha", - "build": "tsc && tsc-alias", + "test": "pnpm run sample && cross-env TZ='Asia/Seoul' pnpm run mocha", + "build": "tsc --project tsconfig.build.json && tsc-alias", "clean": "rimraf dist/", "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node dist/index.js", "lint": "pnpm eslint .", - "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production node", + "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production ts-node --require tsconfig-paths/register", "sample": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/index.js", "dumpDB": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/tools/dump.js", "restoreDB": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/tools/restore.js" diff --git a/scripts/chatPaymentSettlementUpdater.js b/scripts/chatPaymentSettlementUpdater.js index 76e43682..1b1d11dd 100644 --- a/scripts/chatPaymentSettlementUpdater.js +++ b/scripts/chatPaymentSettlementUpdater.js @@ -3,7 +3,7 @@ // https://github.com/sparcs-kaist/taxi-back/issues/449 const { MongoClient } = require("mongodb"); -const { mongo: mongoUrl } = require("../loadenv"); +const { mongo: mongoUrl } = require("@/loadenv"); const client = new MongoClient(mongoUrl); const db = client.db("taxi"); diff --git a/scripts/profileImageUrlUpdater.js b/scripts/profileImageUrlUpdater.js index 2b35bf1a..5814f609 100644 --- a/scripts/profileImageUrlUpdater.js +++ b/scripts/profileImageUrlUpdater.js @@ -2,7 +2,7 @@ // https://github.com/sparcs-kaist/taxi-back/issues/173 const { MongoClient } = require("mongodb"); -const { mongo: mongoUrl, aws: awsEnv } = require("../loadenv"); // FIXME: 올바른 경로로 수정해야 합니다. +const { mongo: mongoUrl, aws: awsEnv } = require("@/loadenv"); const time = Date.now(); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..68db80dd --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], +} diff --git a/tsconfig.json b/tsconfig.json index e7575db4..166f2de6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "@/*": ["./*"] } }, - "include": ["src"], + "include": ["src", "scripts"], "exclude": ["dist", "node_modules"], "ts-node": { "files": true From 7e5b85c68263e1caafc697b20f1027e92198af23 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 27 Nov 2024 03:13:26 +0900 Subject: [PATCH 141/151] Refactor: unused fs --- src/sampleGenerator/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sampleGenerator/index.js b/src/sampleGenerator/index.js index c1168dab..38481739 100644 --- a/src/sampleGenerator/index.js +++ b/src/sampleGenerator/index.js @@ -9,7 +9,6 @@ const { mongo: mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); const database = connectDatabase(mongoUrl); -const fs = require("fs"); const sampleData = require("./sampleData.json"); const main = async () => { From 8d8bac81b05b8f4f5b51d65d4220cf73396993c0 Mon Sep 17 00:00:00 2001 From: neymar <0208mjkim@gmail.com> Date: Wed, 27 Nov 2024 03:19:09 +0900 Subject: [PATCH 142/151] fix: user null --- src/services/users.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/services/users.ts b/src/services/users.ts index a51ac859..fe1e920a 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -16,19 +16,17 @@ export const agreeOnTermsOfServiceHandler: RequestHandler = async ( ) => { try { let user = await userModel.findOne({ id: req.userId }); - if (user && user.agreeOnTermsOfService !== true) { - user.agreeOnTermsOfService = true; - await user.save(); - return res - .status(200) - .send( - "Users/agreeOnTermsOfService : agree on Terms of Service successful" - ); - } else { - return res - .status(400) - .send("Users/agreeOnTermsOfService : already agreed"); + if (!user) { + return res.status(500).send("Users/agreeOnTermsOfService : no such user"); + } + + if (user.agreeOnTermsOfService === true) { + return res.status(400).send("Users/agreeOnTermsOfService: already agreed"); } + + user.agreeOnTermsOfService = true; + await user.save(); + return res.status(200).send("Users/agreeOnTermsOfService : agree successful"); } catch { return res .status(500) From be4159d865daa61feb2a53cf487f7b1d7e7be8d6 Mon Sep 17 00:00:00 2001 From: neymar <0208mjkim@gmail.com> Date: Wed, 27 Nov 2024 03:31:15 +0900 Subject: [PATCH 143/151] fix: change 500 to 400 --- src/services/users.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/users.ts b/src/services/users.ts index fe1e920a..b57709cc 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -17,7 +17,7 @@ export const agreeOnTermsOfServiceHandler: RequestHandler = async ( try { let user = await userModel.findOne({ id: req.userId }); if (!user) { - return res.status(500).send("Users/agreeOnTermsOfService : no such user"); + return res.status(400).send("Users/agreeOnTermsOfService : no such user"); } if (user.agreeOnTermsOfService === true) { @@ -42,6 +42,11 @@ export const getAgreeOnTermsOfServiceHandler: RequestHandler = async ( const user = await userModel .findOne({ id: req.userId }, "agreeOnTermsOfService") .lean(); + + if (!user) { + return res.status(400).send("Users/agreeOnTermsOfService : no such user"); + } + if (user) { const agreeOnTermsOfService = user.agreeOnTermsOfService === true; return res.json({ agreeOnTermsOfService }); From 5b32009ae514f94b3981adbf6ea5d2355f4be5bf Mon Sep 17 00:00:00 2001 From: neymar <0208mjkim@gmail.com> Date: Wed, 27 Nov 2024 03:39:05 +0900 Subject: [PATCH 144/151] fix: matched response to test case --- src/services/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/users.ts b/src/services/users.ts index b57709cc..be3cffa6 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -26,7 +26,7 @@ export const agreeOnTermsOfServiceHandler: RequestHandler = async ( user.agreeOnTermsOfService = true; await user.save(); - return res.status(200).send("Users/agreeOnTermsOfService : agree successful"); + return res.status(200).send("Users/agreeOnTermsOfService : agree on Terms of Service successful"); } catch { return res .status(500) From 74ba7d242c576aa053d93e2975ccef9397347d73 Mon Sep 17 00:00:00 2001 From: neymar <0208mjkim@gmail.com> Date: Wed, 27 Nov 2024 03:41:41 +0900 Subject: [PATCH 145/151] fix: add space --- src/services/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/users.ts b/src/services/users.ts index be3cffa6..9d16626e 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -21,7 +21,7 @@ export const agreeOnTermsOfServiceHandler: RequestHandler = async ( } if (user.agreeOnTermsOfService === true) { - return res.status(400).send("Users/agreeOnTermsOfService: already agreed"); + return res.status(400).send("Users/agreeOnTermsOfService : already agreed"); } user.agreeOnTermsOfService = true; From 94261d55bce94a6de427cffbc41deabd094a0644 Mon Sep 17 00:00:00 2001 From: neymar <0208mjkim@gmail.com> Date: Wed, 27 Nov 2024 03:48:08 +0900 Subject: [PATCH 146/151] fix: minor stuff --- src/services/users.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/services/users.ts b/src/services/users.ts index 9d16626e..3caf35cf 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -42,15 +42,12 @@ export const getAgreeOnTermsOfServiceHandler: RequestHandler = async ( const user = await userModel .findOne({ id: req.userId }, "agreeOnTermsOfService") .lean(); - if (!user) { return res.status(400).send("Users/agreeOnTermsOfService : no such user"); } - if (user) { - const agreeOnTermsOfService = user.agreeOnTermsOfService === true; - return res.json({ agreeOnTermsOfService }); - } + const agreeOnTermsOfService = user.agreeOnTermsOfService === true; + return res.json({ agreeOnTermsOfService }); } catch { return res .status(500) @@ -234,7 +231,7 @@ export const getBanRecordHandler: RequestHandler = async (req, res) => { // 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 const result = await banModel .find({ - userSid: req.session.loginInfo?.sid, + userSid: req.session.loginInfo!.sid, }) .sort({ expireAt: -1 }); if (!result) From b3bad2340890aef8f080b2598d8fa10333e82811 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 04:10:58 +0900 Subject: [PATCH 147/151] Fix: dockerfile error --- Dockerfile | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0873829c..ae7cc13e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,31 @@ -FROM node:18-alpine +# +# First stage: build the app +# +FROM node:18-alpine as builder WORKDIR /usr/src/app -# Install curl(for taxi-watchtower) and pnpm -RUN apk update && apk add curl && npm install --global pnpm@8.8.0 +# Install pnpm +RUN npm install --global pnpm@8.8.0 # pnpm fetch does require only lockfile COPY pnpm-lock.yaml . +RUN pnpm fetch + +COPY . . +RUN pnpm install --offline +RUN pnpm build -# Note: devDependencies are not fetched -RUN pnpm fetch --prod +# +# Second stage: run the app +# +FROM node:18-alpine + +WORKDIR /usr/src/app -# Copy repository and install dependencies -ADD . ./ -RUN pnpm install --offline --prod +COPY --from=builder /usr/src/app/dist . # Run container EXPOSE 80 ENV PORT 80 -CMD ["pnpm", "run", "serve"] +CMD ["node", "dist/index.js"] From d7ae501bc71956e57a9e9623887ab7e41c58653a Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 04:12:33 +0900 Subject: [PATCH 148/151] Fix: dockerfile error 2 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae7cc13e..41e34a89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # # First stage: build the app # -FROM node:18-alpine as builder +FROM node:18-alpine AS builder WORKDIR /usr/src/app @@ -23,7 +23,7 @@ FROM node:18-alpine WORKDIR /usr/src/app -COPY --from=builder /usr/src/app/dist . +COPY --from=builder /usr/src/app/dist dist # Run container EXPOSE 80 From 6acd1c2f4f022e560d9624bc4a30074a74b84ce7 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 04:15:30 +0900 Subject: [PATCH 149/151] Fix: dockerfile error 3 --- Dockerfile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 41e34a89..3e328249 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,9 +23,17 @@ FROM node:18-alpine WORKDIR /usr/src/app +RUN npm install --global pnpm@8.8.0 + +COPY pnpm-lock.yaml . +RUN pnpm fetch --prod + +COPY package.json . +RUN pnpm install --prod + COPY --from=builder /usr/src/app/dist dist # Run container EXPOSE 80 ENV PORT 80 -CMD ["node", "dist/index.js"] +CMD ["pnpm", "serve"] From a8c91e62ff6ee3ab8de3038b46479418790ccc8e Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 04:24:16 +0900 Subject: [PATCH 150/151] Refactor: use offline install in the second stage --- Dockerfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3e328249..8a1a8894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,15 +23,18 @@ FROM node:18-alpine WORKDIR /usr/src/app +# Install pnpm RUN npm install --global pnpm@8.8.0 +# devDependencies are not fetched COPY pnpm-lock.yaml . RUN pnpm fetch --prod COPY package.json . -RUN pnpm install --prod +RUN pnpm install --offline --prod -COPY --from=builder /usr/src/app/dist dist +# Copy the built app from the previous stage +COPY --from=builder /usr/src/app/dist ./dist # Run container EXPOSE 80 From a9e2f50f1433bf1f3761d5ac2ea9cbd840b3c6ed Mon Sep 17 00:00:00 2001 From: static Date: Wed, 27 Nov 2024 04:28:46 +0900 Subject: [PATCH 151/151] Fix: unintended removal of curl package in dockerfile --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8a1a8894..5ffddaa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ WORKDIR /usr/src/app # Install pnpm RUN npm install --global pnpm@8.8.0 +# Install curl for taxi-watchtower +RUN apk update && apk add curl + # devDependencies are not fetched COPY pnpm-lock.yaml . RUN pnpm fetch --prod