Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#450 택시비용 보여주기 #451

Merged
merged 45 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6b3ecf6
Add: 첫번째 계획 코드 (미사용 예정)
ybmin Feb 6, 2024
1d7a106
Add: Cron 추가 및 기작 수정
ybmin Feb 6, 2024
4de7926
Docs: 주석 수정
ybmin Feb 6, 2024
754396c
Add: 네이버 api용 .env 환경 변수 추가
ybmin Feb 6, 2024
de8eefc
Add: Added validator & use project locations
ybmin Feb 13, 2024
163abbf
Add: 일주일 단위 캐싱
ybmin Mar 9, 2024
9d72e86
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Mar 9, 2024
a8cce54
Refactor: change fare location schema string to object id
ybmin Mar 13, 2024
1120f42
Refactor: express-validator to ajv & swagger docs
ybmin Mar 13, 2024
514e174
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Mar 13, 2024
2ac458a
Refactor: start, goal to from, to
ybmin Mar 13, 2024
840b99e
Fix: init error case
ybmin Mar 19, 2024
86ed106
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Mar 19, 2024
a1bab8d
Refactor: zod migration
ybmin Mar 21, 2024
cd94ed6
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Mar 21, 2024
4c88442
Docs: change to naver api optional
ybmin Mar 21, 2024
f11d01c
Add: module init code & exception case
ybmin Mar 21, 2024
c7603ba
Fix: remove import
ybmin Mar 25, 2024
671a07e
Fix: naver api axios 429 error
ybmin Mar 26, 2024
4dfaa40
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Mar 26, 2024
d9f92ca
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Mar 26, 2024
17ac485
Fix: env file
ybmin Mar 26, 2024
836e49e
Fix: naver api key none to null
ybmin Apr 23, 2024
b63c720
Refactor: review contents
ybmin May 1, 2024
b421a38
Fix: enable commented code
ybmin May 7, 2024
3ea9766
Fix: undo promise resolve
ybmin May 7, 2024
b72bfcc
Docs: comment added
ybmin May 7, 2024
8b29fef
Merge branch 'dev' into #450-택시비용-보여주기
ybmin May 7, 2024
cf1a7c8
Fix: unusual case
ybmin May 7, 2024
5b8ee06
Docs: add fare docs to swagger
chlehdwon May 14, 2024
c1eb307
Merge branch '#450-택시비용-보여주기' of https://github.com/sparcs-kaist/taxi…
chlehdwon May 14, 2024
3b7adfd
Refactor: ts migration
ybmin Jul 7, 2024
a1e6a7e
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Jul 7, 2024
e1b82cb
Add: non credential test case execption
ybmin Jul 9, 2024
f36c436
Fix: code convention
ybmin Jul 9, 2024
57d1237
Revert: ts to js
ybmin Jul 18, 2024
a69f0fd
Add: 초기화시 빈 필드만 채움
ybmin Jul 18, 2024
c7a8d29
Fix: undefined error
ybmin Jul 18, 2024
ef3bc02
Fix: code review
ybmin Jul 19, 2024
b6c80fe
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Jul 19, 2024
ad54172
Remove: unused library
ybmin Jul 19, 2024
99e4262
Merge branch '#450-택시비용-보여주기' of https://github.com/sparcs-kaist/taxi…
ybmin Jul 19, 2024
81a37da
Fix: taxiFareModel is not displayed in admin page
kmc7468 Jul 20, 2024
7c48579
Fix: getTaxiFare -> getTaxiFareHandler
ybmin Jul 21, 2024
4b2ca98
Merge branch 'dev' into #450-택시비용-보여주기
ybmin Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=[방의 개수]
Expand Down
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
3 changes: 3 additions & 0 deletions loadenv.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ 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
};
13 changes: 13 additions & 0 deletions src/modules/fare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 시간을 받아서 30분 단위로 변환해서 반환합니다.
* 요일 정보도 하나로 관리
* @summary 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30))
* @param {Date} time 변환할 시간
*/
const scaledTime = (time) => {
return (
48 * time.getDay() + time.getHours() * 2 + (time.getMinutes() >= 30 ? 1 : 0)
ybmin marked this conversation as resolved.
Show resolved Hide resolved
);
};

module.exports = { scaledTime };
13 changes: 13 additions & 0 deletions src/modules/stores/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ const adminLogSchema = Schema({
}, // 수행 업무
});

const taxiFareSchema = Schema(
{
start: { type: String, required: true }, // 출발지
ybmin marked this conversation as resolved.
Show resolved Hide resolved
goal: { type: String, required: true }, // 도착지
time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30))
fare: { type: Number, default: false }, // 예상 택시 요금
},
{
timestamps: true, // 최근 업데이트 시간 기록용
}
);

mongoose.set("strictQuery", true);

const database = mongoose.connection;
Expand Down Expand Up @@ -225,4 +237,5 @@ module.exports = {
adminIPWhitelistSchema
),
adminLogModel: mongoose.model("AdminLog", adminLogSchema),
taxiFareModel: mongoose.model("TaxiFare", taxiFareSchema),
};
36 changes: 36 additions & 0 deletions src/routes/fare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const express = require("express");
const { query } = require("express-validator");
ybmin marked this conversation as resolved.
Show resolved Hide resolved
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.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;
2 changes: 2 additions & 0 deletions src/schedules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
16 changes: 16 additions & 0 deletions src/schedules/updateMajorTaxiFare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const { scaledTime } = require("../modules/fare");
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);
} catch (err) {
console.log(err);
}
};
27 changes: 27 additions & 0 deletions src/schedules/updateMinorTaxiFare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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으로 저장됨
}
}
}
} catch (err) {
console.log(err);
}
};
187 changes: 187 additions & 0 deletions src/services/fare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
const axios = require("axios");

const { naverCloudApiId, naverCloudApiKey } = require("../../loadenv");
const { taxiFareModel, locationModel } = require("../modules/stores/mongo");
const { scaledTime } = require("../modules/fare");

// 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
* 1. Erase all previous data
* 2. Sets all taxi fare to 0
* @summary 카이스트 본원 <-> 대전역의 경우 48 * 7개의 시간대에 대한 택시 요금을 0으로 설정합니다.
* @summary 카이스트 본원 <-> 대전역 외 나머지 경로, 238개의 경로에 대해서는 한 개의 collection씩 설정하여 fare를 0으로 설정합니다. 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);

for (let skey in location) {
for (let gkey in location) {
if (skey === gkey) continue;
let tableFare = [];
// 카이스트 본원 <-> 대전역의 경우 48*7(=336)개의 시간대에 대한 택시 요금을 0으로 설정
if (
(skey === "카이스트 본원" && gkey === "대전역") ||
(skey === "대전역" && gkey === "카이스트 본원")
) {
for (let i = 0; i < 336; i++) {
tableFare.push({
start: skey,
goal: gkey,
time: i,
fare: 0,
});
}
}
// 카이스트 본원 <-> 대전역외의 경로(238개)에 대해서는 7개(일주일) 씩 collection 지정 설정
else {
for (let i = 0; i < 7; i++) {
tableFare.push({
start: skey,
goal: gkey,
time: i * 48,
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에 대한 택시 요금을 반환합니다.
* @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다.
* @summary 카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다.
* @param {Request} req - 파라미터로 start, goal, time을 받습니다.
* - @param {String} start - 출발지
* - @param {String} goal - 도착지
* - @param {Date} time - 출발 시간 (ISO 8601)
*/
const getTaxiFare = async (req, res) => {
ybmin marked this conversation as resolved.
Show resolved Hide resolved
try {
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)
ybmin marked this conversation as resolved.
Show resolved Hide resolved

// 카이스트 본원 <-> 대전역
if (
(start.koName === "카이스트 본원" && goal.koName === "대전역") ||
(start.koName === "대전역" && goal.koName === "카이스트 본원")
) {
let taxiFare = await taxiFareModel
.findOne(
{
start: start.koName,
goal: goal.koName,
time: sTime,
},
function (err, docs) {
ybmin marked this conversation as resolved.
Show resolved Hide resolved
if (err)
console.log(
"Error occured while finding TaxiFare documents: " + err.message
);
}
)
.clone();
//만일 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`,
{ headers: naverCloudApi }
);
res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare });
} else {
res.json({ fare: taxiFare.fare });
}
}
// 카이스트 본원 <-> 대전역이 아닌 경우
else {
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 || taxiFare.fare === 0) {
let response = await axios.get(
`https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${
ybmin marked this conversation as resolved.
Show resolved Hide resolved
start.longitude + "," + start.latitude
}&goal=${goal.longitude + "," + goal.latitude}&options=traoptimal`,
{ headers: naverCloudApi }
);
res.json({ fare: response.data.route.traoptimal[0].summary.taxiFare });
} else {
res.json({ fare: taxiFare.fare });
}
}
} catch (err) {
console.log(err);
ybmin marked this conversation as resolved.
Show resolved Hide resolved
res.status(500).json({ error: "Failed with exception: " + err.message });
}
};

/**
* 주어진 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))
*/
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
);
}
);
};

module.exports = { initDatabase, getTaxiFare, updateTaxiFare };
Loading