From 6b3ecf67ea70c7796c259c9c021c3f0bb9291cb0 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 6 Feb 2024 21:14:38 +0900 Subject: [PATCH 01/86] =?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 02/86] =?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 03/86] =?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 04/86] =?UTF-8?q?Add:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20api?= =?UTF-8?q?=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 1e1a83f381650be829cb5dac3b67c52a507a28a0 Mon Sep 17 00:00:00 2001 From: Dongwon Choi Date: Mon, 12 Feb 2024 17:12:08 +0000 Subject: [PATCH 05/86] Fix: change commitPayment and commitSettlement --- src/routes/docs/rooms.js | 20 ++++++++++---------- src/routes/rooms.js | 11 ++++++----- src/services/rooms.js | 18 +++++++++--------- test/services/rooms.js | 14 +++++++------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 3292b127..53ebbd5f 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -678,11 +678,11 @@ roomsDocs[`${apiPrefix}/searchByUser`] = { }, }; -roomsDocs[`${apiPrefix}/commitPayment`] = { +roomsDocs[`${apiPrefix}/commitSettlement`] = { post: { tags: [tag], - summary: "방 결제 처리", - description: `해당 방에 요청을 보낸 유저를 결제자로 처리합니다.
+ summary: "방 정산 요청 처리", + description: `해당 방에 요청을 보낸 유저를 결제자로 처리하여, 다른 유저들에게 정산을 요청합니다.
이미 출발한 방에 대해서만 요청을 처리합니다.
방의 \`part\` 배열에서 요청을 보낸 유저의 \`isSettlement\` 속성은 \`paid\`로 설정됩니다.
나머지 유저들의 \`isSettlement\` 속성을 \`send-required\`로 설정합니다.`, @@ -726,7 +726,7 @@ roomsDocs[`${apiPrefix}/commitPayment`] = { }, }, example: { - error: "Rooms/:id/commitPayment : cannot find settlement info", + error: "Rooms/:id/commitSettlement : cannot find settlement info", }, }, }, @@ -744,7 +744,7 @@ roomsDocs[`${apiPrefix}/commitPayment`] = { }, }, example: { - error: "Rooms/:id/commitPayment : internal server error", + error: "Rooms/:id/commitSettlement : internal server error", }, }, }, @@ -753,11 +753,11 @@ roomsDocs[`${apiPrefix}/commitPayment`] = { }, }; -roomsDocs[`${apiPrefix}/commitSettlement`] = { +roomsDocs[`${apiPrefix}/commitPayment`] = { post: { tags: [tag], - summary: "방 정산 완료 처리", - description: `해당 방에 요청을 보낸 유저를 정산 완료로 처리합니다.
+ summary: "방 송금 처리", + description: `해당 방에 요청을 보낸 유저를 송금을 완료한 정산 완료로 처리합니다.
방의 \`part\` 배열에서 요청을 보낸 유저의 \`isSettlement\` 속성은 \`send-required\`에서 \`sent\`로 변경합니다.
방의 참여한 유저들이 모두 정산완료를 하면 방의 \`isOver\` 속성이 \`true\`로 변경되며, 과거 방으로 취급됩니다.`, requestBody: { @@ -800,7 +800,7 @@ roomsDocs[`${apiPrefix}/commitSettlement`] = { }, }, example: { - error: "Rooms/:id/settlement : cannot find settlement info", + error: "Rooms/:id/commitPayment : cannot find settlement info", }, }, }, @@ -818,7 +818,7 @@ roomsDocs[`${apiPrefix}/commitSettlement`] = { }, }, example: { - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitPayment : internal server error", }, }, }, diff --git a/src/routes/rooms.js b/src/routes/rooms.js index be1c3f59..bfba96f7 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -78,19 +78,20 @@ router.post( // 로그인된 사용자의 모든 방들을 반환한다. router.get("/searchByUser", roomHandlers.searchByUserHandler); +// 해당 방에 요청을 보낸 유저의 정산을 처리한다. router.post( - "/commitPayment", + "/commitSettlement", body("roomId").isMongoId(), validator, - roomHandlers.commitPaymentHandler + roomHandlers.commitSettlementHandler ); -// 해당 룸의 요청을 보낸 유저의 정산을 완료로 처리한다. +// 해당 방에 요청을 보낸 유저의 송금을 처리한다. router.post( - "/commitSettlement", + "/commitPayment", body("roomId").isMongoId(), validator, - roomHandlers.settlementHandler + roomHandlers.commitPaymentHandler ); // json으로 수정할 값들을 받아 방의 정보를 수정합니다. diff --git a/src/services/rooms.js b/src/services/rooms.js index a2562c67..2228e29d 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -428,7 +428,7 @@ const searchByUserHandler = async (req, res) => { } }; -const commitPaymentHandler = async (req, res) => { +const commitSettlementHandler = async (req, res) => { try { const user = await userModel.findOne({ id: req.userId }); const { roomId } = req.body; @@ -462,7 +462,7 @@ const commitPaymentHandler = async (req, res) => { if (!roomObject) { return res.status(404).json({ - error: "Rooms/:id/commitPayment : cannot find settlement info", + error: "Rooms/:id/commitSettlement : cannot find settlement info", }); } @@ -475,7 +475,7 @@ const commitPaymentHandler = async (req, res) => { if (userOngoingRoomIndex === -1) { await user.save(); return res.status(500).json({ - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitSettlement : internal server error", }); } user.ongoingRoom.splice(userOngoingRoomIndex, 1); @@ -507,12 +507,12 @@ const commitPaymentHandler = async (req, res) => { } catch (err) { logger.error(err); res.status(500).json({ - error: "Rooms/:id/commitPayment : internal server error", + error: "Rooms/:id/commitSettlement : internal server error", }); } }; -const settlementHandler = async (req, res) => { +const commitPaymentHandler = async (req, res) => { try { const { roomId } = req.body; const user = await userModel.findOne({ id: req.userId }); @@ -540,7 +540,7 @@ const settlementHandler = async (req, res) => { if (!roomObject) { return res.status(404).json({ - error: "Rooms/:id/settlement : cannot find settlement info", + error: "Rooms/:id/commitPayment : cannot find settlement info", }); } @@ -553,7 +553,7 @@ const settlementHandler = async (req, res) => { if (userOngoingRoomIndex === -1) { await user.save(); return res.status(500).json({ - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitPayment : internal server error", }); } user.ongoingRoom.splice(userOngoingRoomIndex, 1); @@ -585,7 +585,7 @@ const settlementHandler = async (req, res) => { } catch (err) { logger.error(err); res.status(500).json({ - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitPayment : internal server error", }); } }; @@ -679,6 +679,6 @@ module.exports = { searchHandler, searchByUserHandler, commitPaymentHandler, - settlementHandler, + commitSettlementHandler, // editHandler, }; diff --git a/test/services/rooms.js b/test/services/rooms.js index e17ae6af..62d05609 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -141,8 +141,8 @@ describe("[rooms] 6.searchByUserHandler", () => { }); // 7. 1분이 지난 후, 정산 정보를 불러옴. 예상과 같은 정보를 불러오는지 확인 -describe("[rooms] 7.commitPaymentHandler", () => { - it("should return information of room and commit payment", async () => { +describe("[rooms] 7.commitsettlementHandler", () => { + it("should return information of room and commit settlement", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ @@ -152,7 +152,7 @@ describe("[rooms] 7.commitPaymentHandler", () => { app, }); let res = httpMocks.createResponse(); - await roomsHandlers.commitPaymentHandler(req, res); + await roomsHandlers.commitSettlementHandler(req, res); const resData = res._getData(); expect(resData).to.has.property("name", "test-room"); @@ -161,9 +161,9 @@ describe("[rooms] 7.commitPaymentHandler", () => { }); }); -// 8. 도착 정보를 불러옴. 예상과 같은 정보를 불러오는지 확인 -describe("[rooms] 8.settlementHandler", () => { - it("should return information of room and set settlement", async () => { +// 8. 송금 후 정산 정보를 불러옴. 예상과 같은 정보를 불러오는지 확인 +describe("[rooms] 8.commitPaymentHandler", () => { + it("should return information of room and commit payment", async () => { const testUser2 = await userModel.findOne({ id: "test2" }); const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ @@ -172,7 +172,7 @@ describe("[rooms] 8.settlementHandler", () => { app, }); let res = httpMocks.createResponse(); - await roomsHandlers.settlementHandler(req, res); + await roomsHandlers.commitPaymentHandler(req, res); const resData = res._getData(); expect(resData).to.has.property("name", "test-room"); From de8eefc2e05edb5be5e956425cce38bf0b9b6029 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 13 Feb 2024 17:35:17 +0900 Subject: [PATCH 06/86] 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 07/86] =?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 08/86] 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 09/86] 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 10/86] 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 1bd982f2f422b2254d2caad98d431876874200f8 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Tue, 19 Mar 2024 21:04:48 +0900 Subject: [PATCH 11/86] Refactor: validate by zod --- src/routes/docs/schemas/roomsSchema.js | 8 ++++++++ src/routes/rooms.js | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/routes/docs/schemas/roomsSchema.js b/src/routes/docs/schemas/roomsSchema.js index 39dcd8bf..a0af753e 100644 --- a/src/routes/docs/schemas/roomsSchema.js +++ b/src/routes/docs/schemas/roomsSchema.js @@ -31,6 +31,14 @@ roomsZod["room"] = z }) .partial({ settlementTotal: true, isOver: true }); +roomsZod["commitSettlement"] = z.object({ + roomId: z.string().regex(objectId), +}); + +roomsZod["commitPayment"] = z.object({ + roomId: z.string().regex(objectId), +}); + const roomsSchema = zodToSchemaObject(roomsZod); module.exports = { roomsSchema, roomsZod }; diff --git a/src/routes/rooms.js b/src/routes/rooms.js index 566f862f..6345fa6f 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -1,5 +1,7 @@ const express = require("express"); const { query, body } = require("express-validator"); +const { validateBody } = require("../middlewares/zod"); +const { roomsZod } = require("./docs/schemas/roomsSchema"); const router = express.Router(); const roomHandlers = require("../services/rooms"); @@ -94,16 +96,14 @@ router.get("/searchByUser", roomHandlers.searchByUserHandler); // 해당 방에 요청을 보낸 유저의 정산을 처리한다. router.post( "/commitSettlement", - body("roomId").isMongoId(), - validator, + validateBody(roomsZod.commitSettlement), roomHandlers.commitSettlementHandler ); // 해당 방에 요청을 보낸 유저의 송금을 처리한다. router.post( "/commitPayment", - body("roomId").isMongoId(), - validator, + validateBody(roomsZod.commitPayment), roomHandlers.commitPaymentHandler ); From 840b99efbe86dd0f4dfa027f785dc61f28c91261 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 19 Mar 2024 23:10:42 +0900 Subject: [PATCH 12/86] 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 11c69cc650deff65b05d8ed629a774c364decb73 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Thu, 21 Mar 2024 16:40:04 +0900 Subject: [PATCH 13/86] 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 14/86] 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 15/86] 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 16/86] 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 17/86] 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 18/86] 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 19/86] 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 295366c46ac8249149c4062061fe253d479d4f95 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Tue, 26 Mar 2024 23:48:47 +0900 Subject: [PATCH 20/86] Refactor: change settlement and payment --- src/lottery/modules/contracts.js | 6 +++--- src/modules/socket.js | 4 ++-- src/routes/docs/chats.js | 2 +- src/services/rooms.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 87a24a84..56188041 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -127,7 +127,7 @@ const completeFirstLoginQuest = async (userId, timestamp) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitPaymentHandler, rooms - settlementHandler + * @usage rooms - commitPaymentHandler, rooms - commitSettlementHandler */ const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { logger.info( @@ -167,7 +167,7 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 정산 요청이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitPaymentHandler + * @usage rooms - commitSettlementHandler */ const completePayingQuest = async (userId, timestamp, roomObject) => { logger.info( @@ -195,7 +195,7 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - settlementHandler + * @usage rooms - paymentHandler */ const completeSendingQuest = async (userId, timestamp, roomObject) => { logger.info( diff --git a/src/modules/socket.js b/src/modules/socket.js index 07806ef4..bfdf95af 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -83,11 +83,11 @@ const getMessageBody = (type, nickname = "", content = "") => { const suffix = "님이 퇴장하였습니다"; return `${ellipsisedNickname} ${suffix}`; } - case "payment": { + case "settlement": { const suffix = "님이 정산을 시작하였습니다"; return `${ellipsisedNickname} ${suffix}`; } - case "settlement": { + case "payment": { const suffix = "님이 송금을 완료하였습니다"; return `${ellipsisedNickname} ${suffix}`; } diff --git a/src/routes/docs/chats.js b/src/routes/docs/chats.js index 0aaa1c6d..f9107201 100644 --- a/src/routes/docs/chats.js +++ b/src/routes/docs/chats.js @@ -211,7 +211,7 @@ chatsDocs[`${apiPrefix}/send`] = { Chat { roomId: ObjectId, //방의 objectId - type: String, // 메시지 종류 ("text": 일반 메시지, "s3img": S3에 업로드된 이미지, "in": 입장 메시지, "out": 퇴장 메시지, "payment": 결제 메시지, "settlement": 정산 완료 메시지, "account": 계좌 전송 메시지) + type: String, // 메시지 종류 ("text": 일반 메시지, "s3img": S3에 업로드된 이미지, "in": 입장 메시지, "out": 퇴장 메시지, "settlement": 정산 메시지, "payment": 송금 메시지, "account": 계좌 전송 메시지) authorId: ObejctId, //작성자의 objectId content: String, // 메시지 내용 (메시지 종류에 따라 포맷이 상이함) time: String(ISO 8601), // ex) 2024-01-08T01:52:00.000Z diff --git a/src/services/rooms.js b/src/services/rooms.js index 23f9cafb..7d9f5f8d 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -616,7 +616,7 @@ const commitSettlementHandler = async (req, res) => { // 결제 채팅을 보냅니다. await emitChatEvent(req.app.get("io"), { roomId, - type: "payment", + type: "settlement", content: user.id, authorId: user._id, }); @@ -694,7 +694,7 @@ const commitPaymentHandler = async (req, res) => { // 정산 채팅을 보냅니다. await emitChatEvent(req.app.get("io"), { roomId, - type: "settlement", + type: "payment", content: user.id, authorId: user._id, }); From 5f58f324be648f9a64626716f25a771a58d0ca78 Mon Sep 17 00:00:00 2001 From: hwmin414 Date: Tue, 26 Mar 2024 23:50:02 +0900 Subject: [PATCH 21/86] 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 22/86] 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 880aa3bf843f0d61215765025e9c49e367ca847c Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Wed, 27 Mar 2024 01:57:40 +0900 Subject: [PATCH 23/86] Add chatPaymentSettlementUpdater.js --- scripts/chatPaymentSettlementUpdater.js | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 scripts/chatPaymentSettlementUpdater.js diff --git a/scripts/chatPaymentSettlementUpdater.js b/scripts/chatPaymentSettlementUpdater.js new file mode 100644 index 00000000..22f2fba3 --- /dev/null +++ b/scripts/chatPaymentSettlementUpdater.js @@ -0,0 +1,37 @@ +// Issue #449-1을 해결하기 위한 DB 마이그레이션 스크립트입니다. +// chat type 중 settlement와 payment를 서로 교체합니다. +// https://github.com/sparcs-kaist/taxi-back/issues/449 + +const { MongoClient } = require("mongodb"); +const { mongo: mongoUrl } = require("../loadenv"); + +const time = Date.now(); + +const client = new MongoClient(mongoUrl); +const db = client.db("taxi"); +const chats = db.collection("chats"); + +async function run() { + try { + for await (const doc of chats.find()) { + // 이미 변환이 완료된 경우에는 Pass합니다. + if (doc.type === "settlement" || doc.type === "payment") { + await chats.findOneAndUpdate( + { _id: doc._id }, + { + $set: { + type: doc.type === "settlement" ? "payment" : "settlement", + }, + } + ); + } + } + } catch (err) { + console.log(err); + } finally { + await client.close(); + } +} +run().then(() => { + console.log("Done!"); +}); From 2c06b1f5ea3e9ea9a4bb162ea22781a6da99c8bc Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Wed, 27 Mar 2024 02:07:01 +0900 Subject: [PATCH 24/86] Fix: paymentHandler to commitPaymentHandler --- src/lottery/modules/contracts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 56188041..93d43e23 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -195,7 +195,7 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - paymentHandler + * @usage rooms - commitPaymentHandler */ const completeSendingQuest = async (userId, timestamp, roomObject) => { logger.info( From b31b7b2d2924e30bafb68f64c1e10b7078155a1d Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Wed, 27 Mar 2024 02:10:06 +0900 Subject: [PATCH 25/86] Fix: rooms.js comment --- src/services/rooms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/rooms.js b/src/services/rooms.js index 7d9f5f8d..a1a61913 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -613,7 +613,7 @@ const commitSettlementHandler = async (req, res) => { await user.save(); - // 결제 채팅을 보냅니다. + // 정산 채팅을 보냅니다. await emitChatEvent(req.app.get("io"), { roomId, type: "settlement", @@ -691,7 +691,7 @@ const commitPaymentHandler = async (req, res) => { await user.save(); - // 정산 채팅을 보냅니다. + // 송금 채팅을 보냅니다. await emitChatEvent(req.app.get("io"), { roomId, type: "payment", From d1a04f5364a9e90e5f620cf3f939242857359157 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Thu, 28 Mar 2024 17:55:36 +0900 Subject: [PATCH 26/86] Remove: comment in chatPaymentSettlementUpdater.js --- scripts/chatPaymentSettlementUpdater.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/chatPaymentSettlementUpdater.js b/scripts/chatPaymentSettlementUpdater.js index 22f2fba3..0fa45b89 100644 --- a/scripts/chatPaymentSettlementUpdater.js +++ b/scripts/chatPaymentSettlementUpdater.js @@ -14,7 +14,6 @@ const chats = db.collection("chats"); async function run() { try { for await (const doc of chats.find()) { - // 이미 변환이 완료된 경우에는 Pass합니다. if (doc.type === "settlement" || doc.type === "payment") { await chats.findOneAndUpdate( { _id: doc._id }, From d4336874b52acf2ce7ab655264e3fb93377563da Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Thu, 28 Mar 2024 17:56:14 +0900 Subject: [PATCH 27/86] Fix: test/rooms.js --- test/services/rooms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/services/rooms.js b/test/services/rooms.js index 62d05609..4e948ad1 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -141,7 +141,7 @@ describe("[rooms] 6.searchByUserHandler", () => { }); // 7. 1분이 지난 후, 정산 정보를 불러옴. 예상과 같은 정보를 불러오는지 확인 -describe("[rooms] 7.commitsettlementHandler", () => { +describe("[rooms] 7.commitSettlementHandler", () => { it("should return information of room and commit settlement", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); const testRoom = await roomModel.findOne({ name: "test-room" }); From 836e49eb99edaafe2cd2cc65b0bbdd8eb8d8ff8e Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 23 Apr 2024 22:11:41 +0900 Subject: [PATCH 28/86] 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 b63c72091dfd5923e7912dc156e26c6885f34a13 Mon Sep 17 00:00:00 2001 From: ybmin Date: Wed, 1 May 2024 22:39:45 +0900 Subject: [PATCH 29/86] 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 30/86] 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 31/86] 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 32/86] 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 33/86] 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 ce501cffebaf219f1ca6bc6fbf9b5eb94eb6e91e Mon Sep 17 00:00:00 2001 From: happycastle <41810556+happycastle114@users.noreply.github.com> Date: Wed, 8 May 2024 03:08:12 +0900 Subject: [PATCH 34/86] Add: Update Logic Change --- src/services/auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/auth.js b/src/services/auth.js index dab7cd64..979aa686 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -58,7 +58,7 @@ const joinus = async (req, userData) => { }; const update = async (userData) => { - const updateInfo = { name: userData.name }; + const updateInfo = { name: userData.name, email: userData.email }; await userModel.updateOne({ id: userData.id }, updateInfo); }; @@ -72,7 +72,7 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { await joinus(req, userData); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } - if (user.name != userData.name) { + if (user.name != userData.name || user.email != userData.email) { await update(userData); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } From c696d7cc94a411765d20422ec33d7af6efdcd6b1 Mon Sep 17 00:00:00 2001 From: happycastle <41810556+happycastle114@users.noreply.github.com> Date: Tue, 14 May 2024 23:23:44 +0900 Subject: [PATCH 35/86] Add: Logger --- src/services/auth.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/auth.js b/src/services/auth.js index 979aa686..ffc941ee 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -60,6 +60,7 @@ const joinus = async (req, userData) => { const update = async (userData) => { const updateInfo = { name: userData.name, email: userData.email }; await userModel.updateOne({ id: userData.id }, updateInfo); + logger.info(`Update user info: ${userData.id}`); }; const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { @@ -72,7 +73,7 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { await joinus(req, userData); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } - if (user.name != userData.name || user.email != userData.email) { + if (user.name !== userData.name || user.email !== userData.email) { await update(userData); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } From 5b8ee062fd0d79d249876358ff4594d7bf515c82 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Tue, 14 May 2024 23:24:56 +0900 Subject: [PATCH 36/86] 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 e1728520b7f4781379b3602edf64e1a2bbd5ca87 Mon Sep 17 00:00:00 2001 From: happycastle <41810556+happycastle114@users.noreply.github.com> Date: Tue, 14 May 2024 23:38:41 +0900 Subject: [PATCH 37/86] Add: kaist info update --- src/services/auth.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/services/auth.js b/src/services/auth.js index ffc941ee..1220a778 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -58,7 +58,11 @@ const joinus = async (req, userData) => { }; const update = async (userData) => { - const updateInfo = { name: userData.name, email: userData.email }; + const updateInfo = { + name: userData.name, + email: userData.email, + subinfo: { kaist: userData.kaist }, + }; await userModel.updateOne({ id: userData.id }, updateInfo); logger.info(`Update user info: ${userData.id}`); }; @@ -73,7 +77,11 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { await joinus(req, userData); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } - if (user.name !== userData.name || user.email !== userData.email) { + if ( + user.name !== userData.name || + user.email !== userData.email || + user.subinfo.kaist !== userData.kaist + ) { await update(userData); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } From 65c50ef005f7a149715106a9bdebbb25d3f9de94 Mon Sep 17 00:00:00 2001 From: happycastle <41810556+happycastle114@users.noreply.github.com> Date: Tue, 14 May 2024 23:56:23 +0900 Subject: [PATCH 38/86] Fix: Change all field --- src/services/auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/auth.js b/src/services/auth.js index 1220a778..b23efe6b 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -61,7 +61,7 @@ const update = async (userData) => { const updateInfo = { name: userData.name, email: userData.email, - subinfo: { kaist: userData.kaist }, + "subinfo.kaist": userData.kaist, }; await userModel.updateOne({ id: userData.id }, updateInfo); logger.info(`Update user info: ${userData.id}`); @@ -71,7 +71,7 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { try { const user = await userModel.findOne( { id: userData.id }, - "_id name id withdraw ban" + "_id name email subinfo id withdraw ban" ); if (!user) { await joinus(req, userData); From e3a4ad68879396881391e65464caed4eb8a40105 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Mon, 20 May 2024 23:36:35 +0900 Subject: [PATCH 39/86] Add: check whether user participated in rooms with send-required --- src/routes/docs/rooms.js | 10 +++ src/services/rooms.js | 142 +++++++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 52 deletions(-) diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 710bf649..68b724e7 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -98,6 +98,11 @@ roomsDocs[`${apiPrefix}/create`] = { error: "Rooms/create : participating in too many rooms", }, }, + "사용자가 아직 송금하지 않은 방이 존재": { + value: { + error: "Rooms/create : user has send-required rooms", + }, + }, }, }, }, @@ -309,6 +314,11 @@ roomsDocs[`${apiPrefix}/join`] = { error: "Rooms/join : participating in too many rooms", }, }, + "사용자가 아직 송금하지 않은 방이 존재": { + value: { + error: "Rooms/join : user has send-required rooms", + }, + }, "입력한 시간의 방이 이미 출발함": { value: { error: "Rooms/join : The room has already departed", diff --git a/src/services/rooms.js b/src/services/rooms.js index cf8b6e9e..afaf398e 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -72,6 +72,14 @@ const createHandler = async (req, res) => { }); } + // 사용자가 참여한 진행중인 방 중 송금을 아직 완료하지 않은 방이 있다면 오류를 반환합니다. + const isSendRequired = checkIsSendRequired(user); + if (isSendRequired) { + return res.status(400).json({ + error: "Rooms/create : user has send-required rooms", + }); + } + const part = [{ user: user._id }]; // settlementStatus는 기본적으로 "not-departed"로 설정됨 let room = new roomModel({ @@ -114,58 +122,6 @@ const createHandler = async (req, res) => { } }; -const checkIsAbusing = ( - { from, to, time, maxPartLength }, - countRecentlyMadeRooms, - candidateRooms -) => { - /** - * 방을 생성하였을 때, 다음 조건 중 하나라도 만족하게 되면 어뷰징 가능성이 있다고 판단합니다. - * 1. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 3개 이상인 경우 - * 2. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 2개이고, 다음 조건 중 하나 이상을 만족하는 경우 - * a. 두 방의 출발 시간 간격이 1시간 이하인 경우 - * b. 두 방의 출발 시간 간격이 1시간 초과이고, 다음 조건 중 하나 이상을 만족하는 경우 - * i. 두 방의 출발지가 같은 경우 - * ii. 두 방의 목적지가 같은 경우 - * iii. 먼저 출발하는 방의 목적지와 나중에 출발하는 방의 출발지가 다른 경우 - * iv. 두 방의 최대 탑승 가능 인원이 모두 2명인 경우 - * 3. 최근 24시간 내에 생성한 방이 4개 이상인 경우 - */ - - if (countRecentlyMadeRooms + 1 >= 4) return true; // 조건 3 - - if (candidateRooms.length + 1 >= 3) return true; // 조건 1 - if (candidateRooms.length + 1 < 2) return false; // 조건 2의 여집합 - - let firstRoom = { - from: candidateRooms[0].from.toString(), - to: candidateRooms[0].to.toString(), - time: candidateRooms[0].time, - maxPartLength: candidateRooms[0].maxPartLength, - }; - let secondRoom = { - from, - to, - time: new Date(time), - maxPartLength, - }; - if (secondRoom.time < firstRoom.time) { - [firstRoom, secondRoom] = [secondRoom, firstRoom]; - } - - if (secondRoom.time - firstRoom.time <= 3600000) return true; // 조건 2-a - if ( - firstRoom.from === secondRoom.from || - firstRoom.to === secondRoom.to || - firstRoom.to !== secondRoom.from - ) - return true; // 조건 2-b-i, 2-b-ii, 2-b-iii - if (firstRoom.maxPartLength === 2 && secondRoom.maxPartLength === 2) - return true; // 조건 2-b-iv - - return false; -}; - const createTestHandler = async (req, res) => { // 이 Handler에서는 Parameter에 대해 추가적인 Validation을 하지 않습니다. const { time } = req.body; @@ -288,6 +244,14 @@ const joinHandler = async (req, res) => { }); } + // 사용자가 참여한 진행중인 방 중 송금을 아직 완료하지 않은 방이 있다면 오류를 반환합니다. + const isSendRequired = checkIsSendRequired(user); + if (isSendRequired) { + return res.status(400).json({ + error: "Rooms/join : user has send-required rooms", + }); + } + const room = await roomModel.findById(req.body.roomId); if (!room) { res.status(404).json({ @@ -721,6 +685,80 @@ const settlementHandler = async (req, res) => { } }; +const checkIsAbusing = ( + { from, to, time, maxPartLength }, + countRecentlyMadeRooms, + candidateRooms +) => { + /** + * 방을 생성하였을 때, 다음 조건 중 하나라도 만족하게 되면 어뷰징 가능성이 있다고 판단합니다. + * 1. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 3개 이상인 경우 + * 2. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 2개이고, 다음 조건 중 하나 이상을 만족하는 경우 + * a. 두 방의 출발 시간 간격이 1시간 이하인 경우 + * b. 두 방의 출발 시간 간격이 1시간 초과이고, 다음 조건 중 하나 이상을 만족하는 경우 + * i. 두 방의 출발지가 같은 경우 + * ii. 두 방의 목적지가 같은 경우 + * iii. 먼저 출발하는 방의 목적지와 나중에 출발하는 방의 출발지가 다른 경우 + * iv. 두 방의 최대 탑승 가능 인원이 모두 2명인 경우 + * 3. 최근 24시간 내에 생성한 방이 4개 이상인 경우 + */ + + if (countRecentlyMadeRooms + 1 >= 4) return true; // 조건 3 + + if (candidateRooms.length + 1 >= 3) return true; // 조건 1 + if (candidateRooms.length + 1 < 2) return false; // 조건 2의 여집합 + + let firstRoom = { + from: candidateRooms[0].from.toString(), + to: candidateRooms[0].to.toString(), + time: candidateRooms[0].time, + maxPartLength: candidateRooms[0].maxPartLength, + }; + let secondRoom = { + from, + to, + time: new Date(time), + maxPartLength, + }; + if (secondRoom.time < firstRoom.time) { + [firstRoom, secondRoom] = [secondRoom, firstRoom]; + } + + if (secondRoom.time - firstRoom.time <= 3600000) return true; // 조건 2-a + if ( + firstRoom.from === secondRoom.from || + firstRoom.to === secondRoom.to || + firstRoom.to !== secondRoom.from + ) + return true; // 조건 2-b-i, 2-b-ii, 2-b-iii + if (firstRoom.maxPartLength === 2 && secondRoom.maxPartLength === 2) + return true; // 조건 2-b-iv + + return false; +}; + +/** + * User Object가 주어졌을 때, 해당 유저가 참여한 방 중 아직 유저가 송금하지 않은 방이 있는지 확인합니다. + * @param {Object} userObject - userObject입니다. ongoingRoom 정보를 포함한 형태의 object여야 합니다. + * @return {Boolean} 송금해야 하는 방이 있는지 여부를 반환합니다. + */ +const checkIsSendRequired = async (userObject) => { + // user의 참여중인 방의 part 정보만 가져오기 + const ongoingRoomParts = userObject.ongoingRoom.map((room) => room.part); + // part에서 자신의 id에 해당하는 part만 가져오기 + const userParts = ongoingRoomParts + .map((partList) => + partList.filter((part) => part.user.equals(userObject._id)) + ) + .filter((partList) => partList.length > 0); + // 해당 part object들 중 settlementStatus가 "send-required"인 part 찾기 + const sendRequired = userParts + .map((partList) => partList[0].settlementStatus) + .filter((status) => status === "send-required"); + + return sendRequired.length > 0; +}; + /** * @todo Unused -> Maybe used in the future? */ From 1ba4833b5495e87d964769af85403994185ef988 Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Mon, 20 May 2024 23:45:16 +0900 Subject: [PATCH 40/86] Fix: remove async in checkIsSendRequired --- src/services/rooms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/rooms.js b/src/services/rooms.js index afaf398e..2e478e1b 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -742,7 +742,7 @@ const checkIsAbusing = ( * @param {Object} userObject - userObject입니다. ongoingRoom 정보를 포함한 형태의 object여야 합니다. * @return {Boolean} 송금해야 하는 방이 있는지 여부를 반환합니다. */ -const checkIsSendRequired = async (userObject) => { +const checkIsSendRequired = (userObject) => { // user의 참여중인 방의 part 정보만 가져오기 const ongoingRoomParts = userObject.ongoingRoom.map((room) => room.part); // part에서 자신의 id에 해당하는 part만 가져오기 From dc99335df3e2ad558cbf15832230e35abe4ba72d Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 21 May 2024 01:52:54 +0900 Subject: [PATCH 41/86] Add: isBanned, getBanned api implementation --- src/modules/stores/mongo.js | 37 ++++++++++++++++++++++++++++++++++++ src/routes/admin.js | 2 ++ src/routes/users.js | 6 ++++++ src/services/users.js | 38 ++++++++++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index bf2a8c53..45db1f9c 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -26,6 +26,42 @@ const userSchema = Schema({ account: { type: String, default: "" }, //계좌번호 정보 }); +const banSchema = Schema({ + // 정지 시킬 사용자를 기제함. + userId: { type: mongoose.Types.ObjectId, ref: "User", required: true }, + // 정지 사유 + reason: { + type: String, + default: null, + }, + bannedAt: { + type: Date, // 정지 당한 시각 + required: true, + default: null, + }, + expireAt: { + type: Date, // 정지 만료 시각 + required: true, + default: null, + }, + services: [ + { + // 정지를 당한 서비스를 기제함 + serviceName: { + type: String, + required: true, + // 필요시 이곳에 정지를 시킬 서비스를 추가함. + enum: [ + "all", // all -> 과거/미래 모든 서비스 및 이벤트 이용 제한 + "service", // service -> 방 생성/참여 제한 + "2023-fall-event", // event -> 특정 이벤트 참여 제한 + ], + default: null, + }, + }, + ], +}); + const participantSchema = Schema({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, settlementStatus: { @@ -207,6 +243,7 @@ const connectDatabase = (mongoUrl) => { module.exports = { connectDatabase, userModel: mongoose.model("User", userSchema), + banModel: mongoose.model("Ban", banSchema), deviceTokenModel: mongoose.model("DeviceToken", deviceTokenSchema), notificationOptionModel: mongoose.model( "NotificationOption", diff --git a/src/routes/admin.js b/src/routes/admin.js index da20d60d..7cd28735 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -4,6 +4,7 @@ const AdminJSExpress = require("@adminjs/express"); const AdminJSMongoose = require("@adminjs/mongoose"); const { userModel, + banModel, roomModel, locationModel, chatModel, @@ -26,6 +27,7 @@ AdminJS.registerAdapter(AdminJSMongoose); const resources = [ userModel, + banModel, roomModel, locationModel, chatModel, diff --git a/src/routes/users.js b/src/routes/users.js index 31bde597..2006df25 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -56,4 +56,10 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler); // 프로필 이미지를 기본값으로 재설정합니다. router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); +// 유저의 서비스 정지 여부를 반환합니다. +router.get("/isBanned", userHandlers.isBanned); + +// 유저의 서비스 정지 여부를 반환합니다. +router.get("/getBanRecord", userHandlers.getBanRecord); + module.exports = router; diff --git a/src/services/users.js b/src/services/users.js index 514f2b4a..486eaf2e 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,4 +1,4 @@ -const { userModel } = require("../modules/stores/mongo"); +const { userModel, banModel } = require("../modules/stores/mongo"); const logger = require("../modules/logger"); const aws = require("../modules/stores/aws"); @@ -200,6 +200,40 @@ const resetProfileImgHandler = async (req, res) => { } }; +const isBanned = async (req, res) => { + try { + // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 + const now = Date.now(); + const result = await banModel.find({ + userId: req.userOid, + expireAt: { + $gte: now, + }, + }); + if (!result) { + return res.status(400).json("Users/isBanned : there is no ban record"); + } + console.log(result); + res.status(200).json({ result }); + } catch (err) { + res.status(500).send("Users/isBanned : internal server error"); + } +}; + +const getBanRecord = async (req, res) => { + try { + // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 + const result = await banModel.find({ userId: req.userOid }); + if (!result) { + return res.status(400).json("Users/isBanned : there is no ban record"); + } + console.log(result); + res.status(200).json({ result }); + } catch (err) { + res.status(500).send("Users/isBanned : internal server error"); + } +}; + module.exports = { agreeOnTermsOfServiceHandler, getAgreeOnTermsOfServiceHandler, @@ -209,4 +243,6 @@ module.exports = { editProfileImgDoneHandler, resetNicknameHandler, resetProfileImgHandler, + isBanned, + getBanRecord, }; From 0e63ae7172377e02bf19478e3baf3b7f2dad16b1 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 21 May 2024 02:34:06 +0900 Subject: [PATCH 42/86] Add: swagger docs --- src/routes/docs/users.js | 138 +++++++++++++++++++++++++++++++++++++++ src/services/users.js | 6 +- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index 3cd8aa96..b85b3d27 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -329,4 +329,142 @@ usersDocs[`${apiPrefix}/resetProfileImg`] = { }, }; +usersDocs[`${apiPrefix}/isBanned`] = { + post: { + tags: [tag], + summary: "본인의 현재 정지 기록을 가져움", + description: + "정지 기록들 중 본인이고, 서버 시간을 기준으로 expireAt 보다 작은 경우에 해당하는 정지 기록을 모두 가져옴", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "array", + items: { + properties: { + userId: { + type: "string", + description: "사용자의 UUID", + example: "664b5c1bfd906295bc0639a9", + }, + 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", + }, + services: { + type: "array", + items: { + properties: { + serviceName: { + type: "string", + description: "정지를 당한 서비스 또는 이벤트 이름", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + 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`] = { + post: { + tags: [tag], + summary: "본인의 모든 정지 기록을 가져움", + description: + "정지 기록들 중 본인인 경우에 해당하는 정지 기록을 모두 가져옴", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "array", + items: { + properties: { + userId: { + type: "string", + description: "사용자의 UUID", + example: "664b5c1bfd906295bc0639a9", + }, + 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", + }, + services: { + type: "array", + items: { + properties: { + serviceName: { + type: "string", + description: "정지를 당한 서비스 또는 이벤트 이름", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + 400: { + content: { + "text/html": { + example: "Users/getBanRecord : there is no ban record", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/getBanRecord : internal server error", + }, + }, + }, + }, + }, +}; + module.exports = usersDocs; diff --git a/src/services/users.js b/src/services/users.js index 486eaf2e..377ddea0 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -225,12 +225,14 @@ const getBanRecord = async (req, res) => { // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 const result = await banModel.find({ userId: req.userOid }); if (!result) { - return res.status(400).json("Users/isBanned : there is no ban record"); + return res + .status(400) + .json("Users/getBanRecord : there is no ban record"); } console.log(result); res.status(200).json({ result }); } catch (err) { - res.status(500).send("Users/isBanned : internal server error"); + res.status(500).send("Users/getBanRecord : internal server error"); } }; From 90014131f5f314001a7a9a215d01474178a8739c Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 21 May 2024 02:36:23 +0900 Subject: [PATCH 43/86] Fix: swagger docs --- src/routes/docs/users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index b85b3d27..aba7dd60 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -330,7 +330,7 @@ usersDocs[`${apiPrefix}/resetProfileImg`] = { }; usersDocs[`${apiPrefix}/isBanned`] = { - post: { + get: { tags: [tag], summary: "본인의 현재 정지 기록을 가져움", description: @@ -399,7 +399,7 @@ usersDocs[`${apiPrefix}/isBanned`] = { }; usersDocs[`${apiPrefix}/getBanRecord`] = { - post: { + get: { tags: [tag], summary: "본인의 모든 정지 기록을 가져움", description: From 28be0f0e7723be28111be7b7cff816201e85ec95 Mon Sep 17 00:00:00 2001 From: happycastle <41810556+happycastle114@users.noreply.github.com> Date: Tue, 21 May 2024 23:14:46 +0900 Subject: [PATCH 44/86] Add: More logs --- src/services/auth.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/auth.js b/src/services/auth.js index b23efe6b..15e4c0e7 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -64,7 +64,9 @@ const update = async (userData) => { "subinfo.kaist": userData.kaist, }; await userModel.updateOne({ id: userData.id }, updateInfo); - logger.info(`Update user info: ${userData.id}`); + logger.info( + `Update user info: ${userData.id} ${userData.name} ${userData.email} ${userData.kaist}` + ); }; const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { @@ -83,6 +85,9 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { user.subinfo.kaist !== userData.kaist ) { await update(userData); + logger.info( + `Past user info: ${user.id} ${user.name} ${user.email} ${user.subinfo.kaist}` + ); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } From 75b5314d2ce6b1d4e87bd4c134f3389913ab5d5a Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 28 May 2024 01:11:36 +0900 Subject: [PATCH 45/86] Refactor: code convention and resolve comments --- src/modules/stores/mongo.js | 1 + src/routes/docs/users.js | 5 +++-- src/routes/users.js | 4 ++-- src/services/users.js | 13 +++++-------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 45db1f9c..2c9d3748 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -33,6 +33,7 @@ const banSchema = Schema({ reason: { type: String, default: null, + required: true, }, bannedAt: { type: Date, // 정지 당한 시각 diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index aba7dd60..ac8fa865 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -1,5 +1,6 @@ const tag = "users"; const apiPrefix = "/users"; +const { objectId } = require("../../modules/patterns"); const usersDocs = {}; usersDocs[`${apiPrefix}/agreeOnTermsOfService`] = { @@ -346,7 +347,7 @@ usersDocs[`${apiPrefix}/isBanned`] = { userId: { type: "string", description: "사용자의 UUID", - example: "664b5c1bfd906295bc0639a9", + pattern: objectId.source, }, reason: { type: "string", @@ -415,7 +416,7 @@ usersDocs[`${apiPrefix}/getBanRecord`] = { userId: { type: "string", description: "사용자의 UUID", - example: "664b5c1bfd906295bc0639a9", + pattern: objectId.source, }, reason: { type: "string", diff --git a/src/routes/users.js b/src/routes/users.js index 2006df25..c81534a7 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -57,9 +57,9 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler); router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); // 유저의 서비스 정지 여부를 반환합니다. -router.get("/isBanned", userHandlers.isBanned); +router.get("/isBanned", userHandlers.isBannedHandler); // 유저의 서비스 정지 여부를 반환합니다. -router.get("/getBanRecord", userHandlers.getBanRecord); +router.get("/getBanRecord", userHandlers.getBanRecordHandler); module.exports = router; diff --git a/src/services/users.js b/src/services/users.js index 377ddea0..b65dfa31 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -200,27 +200,25 @@ const resetProfileImgHandler = async (req, res) => { } }; -const isBanned = async (req, res) => { +const isBannedHandler = async (req, res) => { try { // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 - const now = Date.now(); const result = await banModel.find({ userId: req.userOid, expireAt: { - $gte: now, + $gte: req.timestamp, }, }); if (!result) { return res.status(400).json("Users/isBanned : there is no ban record"); } - console.log(result); res.status(200).json({ result }); } catch (err) { res.status(500).send("Users/isBanned : internal server error"); } }; -const getBanRecord = async (req, res) => { +const getBanRecordHandler = async (req, res) => { try { // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 const result = await banModel.find({ userId: req.userOid }); @@ -229,7 +227,6 @@ const getBanRecord = async (req, res) => { .status(400) .json("Users/getBanRecord : there is no ban record"); } - console.log(result); res.status(200).json({ result }); } catch (err) { res.status(500).send("Users/getBanRecord : internal server error"); @@ -245,6 +242,6 @@ module.exports = { editProfileImgDoneHandler, resetNicknameHandler, resetProfileImgHandler, - isBanned, - getBanRecord, + isBannedHandler, + getBanRecordHandler, }; From fc365c46db3182891c46fb960970e442b9746b31 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 28 May 2024 03:03:52 +0900 Subject: [PATCH 46/86] Refactor: resolve comments --- src/modules/stores/mongo.js | 4 ---- src/routes/users.js | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 2c9d3748..6bc93246 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -32,18 +32,15 @@ const banSchema = Schema({ // 정지 사유 reason: { type: String, - default: null, required: true, }, bannedAt: { type: Date, // 정지 당한 시각 required: true, - default: null, }, expireAt: { type: Date, // 정지 만료 시각 required: true, - default: null, }, services: [ { @@ -57,7 +54,6 @@ const banSchema = Schema({ "service", // service -> 방 생성/참여 제한 "2023-fall-event", // event -> 특정 이벤트 참여 제한 ], - default: null, }, }, ], diff --git a/src/routes/users.js b/src/routes/users.js index c81534a7..d5366155 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -56,10 +56,10 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler); // 프로필 이미지를 기본값으로 재설정합니다. router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); -// 유저의 서비스 정지 여부를 반환합니다. +// 유저의 현재 유효한 서비스 정지 기록들만 반환합니다. router.get("/isBanned", userHandlers.isBannedHandler); -// 유저의 서비스 정지 여부를 반환합니다. +// 유저의 서비스 정지 기록들을 모두 반환합니다. router.get("/getBanRecord", userHandlers.getBanRecordHandler); module.exports = router; From 2bd12af6106f4289942175852af44d2799bc0834 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 28 May 2024 03:45:07 +0900 Subject: [PATCH 47/86] Refactor: resolve comments --- src/routes/docs/users.js | 4 ++-- src/services/users.js | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index ac8fa865..deaeb113 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -346,7 +346,7 @@ usersDocs[`${apiPrefix}/isBanned`] = { properties: { userId: { type: "string", - description: "사용자의 UUID", + description: "사용자의 ObjectId", pattern: objectId.source, }, reason: { @@ -415,7 +415,7 @@ usersDocs[`${apiPrefix}/getBanRecord`] = { properties: { userId: { type: "string", - description: "사용자의 UUID", + description: "사용자의 ObjectId", pattern: objectId.source, }, reason: { diff --git a/src/services/users.js b/src/services/users.js index b65dfa31..2fd8eec5 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -209,10 +209,9 @@ const isBannedHandler = async (req, res) => { $gte: req.timestamp, }, }); - if (!result) { - return res.status(400).json("Users/isBanned : there is no ban record"); - } - res.status(200).json({ result }); + if (!result) + return res.status(500).send("Users/getBanRecord : internal server error"); + res.status(200).json(result); } catch (err) { res.status(500).send("Users/isBanned : internal server error"); } @@ -222,12 +221,9 @@ const getBanRecordHandler = async (req, res) => { try { // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 const result = await banModel.find({ userId: req.userOid }); - if (!result) { - return res - .status(400) - .json("Users/getBanRecord : there is no ban record"); - } - res.status(200).json({ result }); + if (!result) + return res.status(500).send("Users/getBanRecord : internal server error"); + res.status(200).json(result); } catch (err) { res.status(500).send("Users/getBanRecord : internal server error"); } From 323c09a8f26ed3ed867acc4f62d770c2e1140f40 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 28 May 2024 03:46:35 +0900 Subject: [PATCH 48/86] Fix: wrong comment --- src/services/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/users.js b/src/services/users.js index 2fd8eec5..5d8ee632 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -210,7 +210,7 @@ const isBannedHandler = async (req, res) => { }, }); if (!result) - return res.status(500).send("Users/getBanRecord : internal server error"); + 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"); From f9eb6013a0a071751156b653281093cd9510bcc1 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 28 May 2024 22:46:29 +0900 Subject: [PATCH 49/86] =?UTF-8?q?Revert=20"#517=20=EB=AF=B8=EC=A0=95?= =?UTF-8?q?=EC=82=B0=20=EC=8B=9C=20=EB=B0=A9=20=EC=83=9D=EC=84=B1/?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20=EC=B0=A8=EB=8B=A8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/docs/rooms.js | 10 --- src/services/rooms.js | 142 ++++++++++++++------------------------- 2 files changed, 52 insertions(+), 100 deletions(-) diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 68b724e7..710bf649 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -98,11 +98,6 @@ roomsDocs[`${apiPrefix}/create`] = { error: "Rooms/create : participating in too many rooms", }, }, - "사용자가 아직 송금하지 않은 방이 존재": { - value: { - error: "Rooms/create : user has send-required rooms", - }, - }, }, }, }, @@ -314,11 +309,6 @@ roomsDocs[`${apiPrefix}/join`] = { error: "Rooms/join : participating in too many rooms", }, }, - "사용자가 아직 송금하지 않은 방이 존재": { - value: { - error: "Rooms/join : user has send-required rooms", - }, - }, "입력한 시간의 방이 이미 출발함": { value: { error: "Rooms/join : The room has already departed", diff --git a/src/services/rooms.js b/src/services/rooms.js index 2e478e1b..cf8b6e9e 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -72,14 +72,6 @@ const createHandler = async (req, res) => { }); } - // 사용자가 참여한 진행중인 방 중 송금을 아직 완료하지 않은 방이 있다면 오류를 반환합니다. - const isSendRequired = checkIsSendRequired(user); - if (isSendRequired) { - return res.status(400).json({ - error: "Rooms/create : user has send-required rooms", - }); - } - const part = [{ user: user._id }]; // settlementStatus는 기본적으로 "not-departed"로 설정됨 let room = new roomModel({ @@ -122,6 +114,58 @@ const createHandler = async (req, res) => { } }; +const checkIsAbusing = ( + { from, to, time, maxPartLength }, + countRecentlyMadeRooms, + candidateRooms +) => { + /** + * 방을 생성하였을 때, 다음 조건 중 하나라도 만족하게 되면 어뷰징 가능성이 있다고 판단합니다. + * 1. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 3개 이상인 경우 + * 2. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 2개이고, 다음 조건 중 하나 이상을 만족하는 경우 + * a. 두 방의 출발 시간 간격이 1시간 이하인 경우 + * b. 두 방의 출발 시간 간격이 1시간 초과이고, 다음 조건 중 하나 이상을 만족하는 경우 + * i. 두 방의 출발지가 같은 경우 + * ii. 두 방의 목적지가 같은 경우 + * iii. 먼저 출발하는 방의 목적지와 나중에 출발하는 방의 출발지가 다른 경우 + * iv. 두 방의 최대 탑승 가능 인원이 모두 2명인 경우 + * 3. 최근 24시간 내에 생성한 방이 4개 이상인 경우 + */ + + if (countRecentlyMadeRooms + 1 >= 4) return true; // 조건 3 + + if (candidateRooms.length + 1 >= 3) return true; // 조건 1 + if (candidateRooms.length + 1 < 2) return false; // 조건 2의 여집합 + + let firstRoom = { + from: candidateRooms[0].from.toString(), + to: candidateRooms[0].to.toString(), + time: candidateRooms[0].time, + maxPartLength: candidateRooms[0].maxPartLength, + }; + let secondRoom = { + from, + to, + time: new Date(time), + maxPartLength, + }; + if (secondRoom.time < firstRoom.time) { + [firstRoom, secondRoom] = [secondRoom, firstRoom]; + } + + if (secondRoom.time - firstRoom.time <= 3600000) return true; // 조건 2-a + if ( + firstRoom.from === secondRoom.from || + firstRoom.to === secondRoom.to || + firstRoom.to !== secondRoom.from + ) + return true; // 조건 2-b-i, 2-b-ii, 2-b-iii + if (firstRoom.maxPartLength === 2 && secondRoom.maxPartLength === 2) + return true; // 조건 2-b-iv + + return false; +}; + const createTestHandler = async (req, res) => { // 이 Handler에서는 Parameter에 대해 추가적인 Validation을 하지 않습니다. const { time } = req.body; @@ -244,14 +288,6 @@ const joinHandler = async (req, res) => { }); } - // 사용자가 참여한 진행중인 방 중 송금을 아직 완료하지 않은 방이 있다면 오류를 반환합니다. - const isSendRequired = checkIsSendRequired(user); - if (isSendRequired) { - return res.status(400).json({ - error: "Rooms/join : user has send-required rooms", - }); - } - const room = await roomModel.findById(req.body.roomId); if (!room) { res.status(404).json({ @@ -685,80 +721,6 @@ const settlementHandler = async (req, res) => { } }; -const checkIsAbusing = ( - { from, to, time, maxPartLength }, - countRecentlyMadeRooms, - candidateRooms -) => { - /** - * 방을 생성하였을 때, 다음 조건 중 하나라도 만족하게 되면 어뷰징 가능성이 있다고 판단합니다. - * 1. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 3개 이상인 경우 - * 2. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 2개이고, 다음 조건 중 하나 이상을 만족하는 경우 - * a. 두 방의 출발 시간 간격이 1시간 이하인 경우 - * b. 두 방의 출발 시간 간격이 1시간 초과이고, 다음 조건 중 하나 이상을 만족하는 경우 - * i. 두 방의 출발지가 같은 경우 - * ii. 두 방의 목적지가 같은 경우 - * iii. 먼저 출발하는 방의 목적지와 나중에 출발하는 방의 출발지가 다른 경우 - * iv. 두 방의 최대 탑승 가능 인원이 모두 2명인 경우 - * 3. 최근 24시간 내에 생성한 방이 4개 이상인 경우 - */ - - if (countRecentlyMadeRooms + 1 >= 4) return true; // 조건 3 - - if (candidateRooms.length + 1 >= 3) return true; // 조건 1 - if (candidateRooms.length + 1 < 2) return false; // 조건 2의 여집합 - - let firstRoom = { - from: candidateRooms[0].from.toString(), - to: candidateRooms[0].to.toString(), - time: candidateRooms[0].time, - maxPartLength: candidateRooms[0].maxPartLength, - }; - let secondRoom = { - from, - to, - time: new Date(time), - maxPartLength, - }; - if (secondRoom.time < firstRoom.time) { - [firstRoom, secondRoom] = [secondRoom, firstRoom]; - } - - if (secondRoom.time - firstRoom.time <= 3600000) return true; // 조건 2-a - if ( - firstRoom.from === secondRoom.from || - firstRoom.to === secondRoom.to || - firstRoom.to !== secondRoom.from - ) - return true; // 조건 2-b-i, 2-b-ii, 2-b-iii - if (firstRoom.maxPartLength === 2 && secondRoom.maxPartLength === 2) - return true; // 조건 2-b-iv - - return false; -}; - -/** - * User Object가 주어졌을 때, 해당 유저가 참여한 방 중 아직 유저가 송금하지 않은 방이 있는지 확인합니다. - * @param {Object} userObject - userObject입니다. ongoingRoom 정보를 포함한 형태의 object여야 합니다. - * @return {Boolean} 송금해야 하는 방이 있는지 여부를 반환합니다. - */ -const checkIsSendRequired = (userObject) => { - // user의 참여중인 방의 part 정보만 가져오기 - const ongoingRoomParts = userObject.ongoingRoom.map((room) => room.part); - // part에서 자신의 id에 해당하는 part만 가져오기 - const userParts = ongoingRoomParts - .map((partList) => - partList.filter((part) => part.user.equals(userObject._id)) - ) - .filter((partList) => partList.length > 0); - // 해당 part object들 중 settlementStatus가 "send-required"인 part 찾기 - const sendRequired = userParts - .map((partList) => partList[0].settlementStatus) - .filter((status) => status === "send-required"); - - return sendRequired.length > 0; -}; - /** * @todo Unused -> Maybe used in the future? */ From 0a91f62df548d0c077be12d9b7dbed5f34b7166e Mon Sep 17 00:00:00 2001 From: chlehdwon Date: Tue, 28 May 2024 23:04:46 +0900 Subject: [PATCH 50/86] Fix: minor changes --- scripts/chatPaymentSettlementUpdater.js | 2 -- src/lottery/modules/contracts.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/chatPaymentSettlementUpdater.js b/scripts/chatPaymentSettlementUpdater.js index 0fa45b89..76e43682 100644 --- a/scripts/chatPaymentSettlementUpdater.js +++ b/scripts/chatPaymentSettlementUpdater.js @@ -5,8 +5,6 @@ const { MongoClient } = require("mongodb"); const { mongo: mongoUrl } = require("../loadenv"); -const time = Date.now(); - const client = new MongoClient(mongoUrl); const db = client.db("taxi"); const chats = db.collection("chats"); diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 93d43e23..72a62deb 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -127,7 +127,7 @@ const completeFirstLoginQuest = async (userId, timestamp) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitPaymentHandler, rooms - commitSettlementHandler + * @usage rooms - commitSettlementHandler, rooms - commitPaymentHandler */ const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { logger.info( From 6cedcf91bc7c66487fcc297974265ff70cbba7dd Mon Sep 17 00:00:00 2001 From: happycastle <41810556+happycastle114@users.noreply.github.com> Date: Tue, 28 May 2024 23:31:21 +0900 Subject: [PATCH 51/86] Fix: Change KAIST Info Mail --- src/services/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/auth.js b/src/services/auth.js index 15e4c0e7..fdca15dd 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -33,7 +33,7 @@ const transUserData = (userData) => { twitter: userData.twitter_id || "", kaist: kaistInfo?.ku_std_no || "", sparcs: userData.sparcs_id || "", - email: userData.email, + email: kaistInfo?.mail || userData.email, isEligible: userPattern.allowedEmployeeTypes.test(kaistInfo?.employeeType), }; return info; From 3b7adfd4b1fe0a5f200d85374d4f92f4e91326d9 Mon Sep 17 00:00:00 2001 From: ybmin Date: Sun, 7 Jul 2024 18:58:13 +0900 Subject: [PATCH 52/86] 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 53/86] 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 47671f91ef64c2522abcf9a5905da8495b5609bb Mon Sep 17 00:00:00 2001 From: static Date: Tue, 9 Jul 2024 22:09:34 +0900 Subject: [PATCH 54/86] =?UTF-8?q?Revert=20"Revert=20"#517=20=EB=AF=B8?= =?UTF-8?q?=EC=A0=95=EC=82=B0=20=EC=8B=9C=20=EB=B0=A9=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?/=EC=B0=B8=EC=97=AC=20=EC=B0=A8=EB=8B=A8""?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/docs/rooms.js | 10 +++ src/services/rooms.js | 142 +++++++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 52 deletions(-) diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 87488b11..3ac66e6b 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -98,6 +98,11 @@ roomsDocs[`${apiPrefix}/create`] = { error: "Rooms/create : participating in too many rooms", }, }, + "사용자가 아직 송금하지 않은 방이 존재": { + value: { + error: "Rooms/create : user has send-required rooms", + }, + }, }, }, }, @@ -309,6 +314,11 @@ roomsDocs[`${apiPrefix}/join`] = { error: "Rooms/join : participating in too many rooms", }, }, + "사용자가 아직 송금하지 않은 방이 존재": { + value: { + error: "Rooms/join : user has send-required rooms", + }, + }, "입력한 시간의 방이 이미 출발함": { value: { error: "Rooms/join : The room has already departed", diff --git a/src/services/rooms.js b/src/services/rooms.js index a1a61913..218a23d5 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -72,6 +72,14 @@ const createHandler = async (req, res) => { }); } + // 사용자가 참여한 진행중인 방 중 송금을 아직 완료하지 않은 방이 있다면 오류를 반환합니다. + const isSendRequired = checkIsSendRequired(user); + if (isSendRequired) { + return res.status(400).json({ + error: "Rooms/create : user has send-required rooms", + }); + } + const part = [{ user: user._id }]; // settlementStatus는 기본적으로 "not-departed"로 설정됨 let room = new roomModel({ @@ -114,58 +122,6 @@ const createHandler = async (req, res) => { } }; -const checkIsAbusing = ( - { from, to, time, maxPartLength }, - countRecentlyMadeRooms, - candidateRooms -) => { - /** - * 방을 생성하였을 때, 다음 조건 중 하나라도 만족하게 되면 어뷰징 가능성이 있다고 판단합니다. - * 1. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 3개 이상인 경우 - * 2. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 2개이고, 다음 조건 중 하나 이상을 만족하는 경우 - * a. 두 방의 출발 시간 간격이 1시간 이하인 경우 - * b. 두 방의 출발 시간 간격이 1시간 초과이고, 다음 조건 중 하나 이상을 만족하는 경우 - * i. 두 방의 출발지가 같은 경우 - * ii. 두 방의 목적지가 같은 경우 - * iii. 먼저 출발하는 방의 목적지와 나중에 출발하는 방의 출발지가 다른 경우 - * iv. 두 방의 최대 탑승 가능 인원이 모두 2명인 경우 - * 3. 최근 24시간 내에 생성한 방이 4개 이상인 경우 - */ - - if (countRecentlyMadeRooms + 1 >= 4) return true; // 조건 3 - - if (candidateRooms.length + 1 >= 3) return true; // 조건 1 - if (candidateRooms.length + 1 < 2) return false; // 조건 2의 여집합 - - let firstRoom = { - from: candidateRooms[0].from.toString(), - to: candidateRooms[0].to.toString(), - time: candidateRooms[0].time, - maxPartLength: candidateRooms[0].maxPartLength, - }; - let secondRoom = { - from, - to, - time: new Date(time), - maxPartLength, - }; - if (secondRoom.time < firstRoom.time) { - [firstRoom, secondRoom] = [secondRoom, firstRoom]; - } - - if (secondRoom.time - firstRoom.time <= 3600000) return true; // 조건 2-a - if ( - firstRoom.from === secondRoom.from || - firstRoom.to === secondRoom.to || - firstRoom.to !== secondRoom.from - ) - return true; // 조건 2-b-i, 2-b-ii, 2-b-iii - if (firstRoom.maxPartLength === 2 && secondRoom.maxPartLength === 2) - return true; // 조건 2-b-iv - - return false; -}; - const createTestHandler = async (req, res) => { // 이 Handler에서는 Parameter에 대해 추가적인 Validation을 하지 않습니다. const { time } = req.body; @@ -288,6 +244,14 @@ const joinHandler = async (req, res) => { }); } + // 사용자가 참여한 진행중인 방 중 송금을 아직 완료하지 않은 방이 있다면 오류를 반환합니다. + const isSendRequired = checkIsSendRequired(user); + if (isSendRequired) { + return res.status(400).json({ + error: "Rooms/join : user has send-required rooms", + }); + } + const room = await roomModel.findById(req.body.roomId); if (!room) { res.status(404).json({ @@ -721,6 +685,80 @@ const commitPaymentHandler = async (req, res) => { } }; +const checkIsAbusing = ( + { from, to, time, maxPartLength }, + countRecentlyMadeRooms, + candidateRooms +) => { + /** + * 방을 생성하였을 때, 다음 조건 중 하나라도 만족하게 되면 어뷰징 가능성이 있다고 판단합니다. + * 1. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 3개 이상인 경우 + * 2. 참여한 방 중, 생성하려는 방의 출발 시간 앞 뒤 12시간 내에 출발하는 방이 2개이고, 다음 조건 중 하나 이상을 만족하는 경우 + * a. 두 방의 출발 시간 간격이 1시간 이하인 경우 + * b. 두 방의 출발 시간 간격이 1시간 초과이고, 다음 조건 중 하나 이상을 만족하는 경우 + * i. 두 방의 출발지가 같은 경우 + * ii. 두 방의 목적지가 같은 경우 + * iii. 먼저 출발하는 방의 목적지와 나중에 출발하는 방의 출발지가 다른 경우 + * iv. 두 방의 최대 탑승 가능 인원이 모두 2명인 경우 + * 3. 최근 24시간 내에 생성한 방이 4개 이상인 경우 + */ + + if (countRecentlyMadeRooms + 1 >= 4) return true; // 조건 3 + + if (candidateRooms.length + 1 >= 3) return true; // 조건 1 + if (candidateRooms.length + 1 < 2) return false; // 조건 2의 여집합 + + let firstRoom = { + from: candidateRooms[0].from.toString(), + to: candidateRooms[0].to.toString(), + time: candidateRooms[0].time, + maxPartLength: candidateRooms[0].maxPartLength, + }; + let secondRoom = { + from, + to, + time: new Date(time), + maxPartLength, + }; + if (secondRoom.time < firstRoom.time) { + [firstRoom, secondRoom] = [secondRoom, firstRoom]; + } + + if (secondRoom.time - firstRoom.time <= 3600000) return true; // 조건 2-a + if ( + firstRoom.from === secondRoom.from || + firstRoom.to === secondRoom.to || + firstRoom.to !== secondRoom.from + ) + return true; // 조건 2-b-i, 2-b-ii, 2-b-iii + if (firstRoom.maxPartLength === 2 && secondRoom.maxPartLength === 2) + return true; // 조건 2-b-iv + + return false; +}; + +/** + * User Object가 주어졌을 때, 해당 유저가 참여한 방 중 아직 유저가 송금하지 않은 방이 있는지 확인합니다. + * @param {Object} userObject - userObject입니다. ongoingRoom 정보를 포함한 형태의 object여야 합니다. + * @return {Boolean} 송금해야 하는 방이 있는지 여부를 반환합니다. + */ +const checkIsSendRequired = (userObject) => { + // user의 참여중인 방의 part 정보만 가져오기 + const ongoingRoomParts = userObject.ongoingRoom.map((room) => room.part); + // part에서 자신의 id에 해당하는 part만 가져오기 + const userParts = ongoingRoomParts + .map((partList) => + partList.filter((part) => part.user.equals(userObject._id)) + ) + .filter((partList) => partList.length > 0); + // 해당 part object들 중 settlementStatus가 "send-required"인 part 찾기 + const sendRequired = userParts + .map((partList) => partList[0].settlementStatus) + .filter((status) => status === "send-required"); + + return sendRequired.length > 0; +}; + /** * @todo Unused -> Maybe used in the future? */ From f36c436035bfbd7378416bf348b86ec32e5e3c44 Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 9 Jul 2024 22:10:15 +0900 Subject: [PATCH 55/86] 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 56/86] 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 57/86] =?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 58/86] 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 59/86] 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 60/86] 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 61/86] 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 62/86] 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 de3329f699e329b078302a881cdb53c22dab5f6d Mon Sep 17 00:00:00 2001 From: ybmin Date: Tue, 13 Aug 2024 21:50:21 +0900 Subject: [PATCH 63/86] 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 64/86] 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 65/86] 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 66/86] 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 67/86] 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 68/86] 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 69/86] 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 70/86] 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 71/86] 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 72/86] 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 73/86] 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 74/86] 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 75/86] 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 76/86] 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 77/86] 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 78/86] 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 79/86] 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 80/86] 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 81/86] 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 82/86] 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 83/86] 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 84/86] 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 85/86] 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 86/86] 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,