Skip to content

Commit

Permalink
Merge pull request #451 from sparcs-kaist/#450-택시비용-보여주기
Browse files Browse the repository at this point in the history
#450 택시비용 보여주기
  • Loading branch information
kmc7468 authored Jul 24, 2024
2 parents e348e63 + 4b2ca98 commit 147fc5c
Show file tree
Hide file tree
Showing 15 changed files with 492 additions and 3 deletions.
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
4 changes: 4 additions & 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 All @@ -85,3 +86,6 @@ app.set("io", startSocketServer(serverHttp));

// [Schedule] 스케줄러 시작
require("./src/schedules")(app);

// [Module] 택시 예상 비용 db 초기화
require("./src/modules/fare").initializeDatabase();
4 changes: 4 additions & 0 deletions loadenv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
naverMap: {
apiId: process.env.NAVER_MAP_API_ID, // optional
apiKey: process.env.NAVER_MAP_API_KEY, //optional
},
};
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

208 changes: 208 additions & 0 deletions src/modules/fare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
const axios = require("axios");
const logger = require("./logger");

const { naverMap } = require("../../loadenv");
const { taxiFareModel, locationModel } = require("./stores/mongo");

const naverMapApi = {
"X-NCP-APIGW-API-KEY-ID": naverMap.apiId,
"X-NCP-APIGW-API-KEY": naverMap.apiKey,
};

// 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() +
time.getHours() * 2 +
(time.getMinutes() >= 30 ? 1 : 0)
);
};

/**
* 데이터베이스를 초기화합니다. 존재하지 않는 필드가 있을때, 기존의 값으로 초기화해 놓거나, 아얘 비어있을 경우에 api를 통해 값을 받아와 초기화합니다.
* @returns
*/
const initializeDatabase = async () => {
try {
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 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,
},
{ fare: true }
)
.lean()
).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,
},
});
});
} 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,
},
});
});
}
await taxiFareModel.bulkWrite(tableFare);
await new Promise((resolve) => setTimeout(resolve, 200));
})
);
})
);
} catch (err) {
logger.error("Error occured while initializing database: " + err.message);
}
};

/**
* 주어진 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,
})
.lean();
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,
};
14 changes: 14 additions & 0 deletions src/modules/stores/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,19 @@ const adminLogSchema = Schema({
}, // 수행 업무
});

const taxiFareSchema = Schema(
{
from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 출발지
to: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 도착지
isMajor: { type: Boolean, default: false }, // 카이스트 본원 <-> 대전역 경로 여부
time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30))
fare: { type: Number, default: false }, // 예상 택시 요금
},
{
timestamps: true, // 최근 업데이트 시간 기록용
}
);

mongoose.set("strictQuery", true);

const database = mongoose.connection;
Expand Down Expand Up @@ -259,4 +272,5 @@ module.exports = {
adminIPWhitelistSchema
),
adminLogModel: mongoose.model("AdminLog", adminLogSchema),
taxiFareModel: mongoose.model("TaxiFare", taxiFareSchema),
};
2 changes: 2 additions & 0 deletions src/routes/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
adminLogModel,
deviceTokenModel,
notificationOptionModel,
taxiFareModel,
} = require("../modules/stores/mongo");
const { buildResource } = require("../modules/adminResource");

Expand All @@ -36,6 +37,7 @@ const resources = [
adminLogModel,
deviceTokenModel,
notificationOptionModel,
taxiFareModel,
]
.map(buildResource())
.concat(require("../lottery").resources);
Expand Down
54 changes: 54 additions & 0 deletions src/routes/docs/fare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const { objectIdPattern } = require("./utils");

const tag = "fare";
const apiPrefix = "/fare";

const fareDocs = {};

fareDocs[`${apiPrefix}/getTaxiFare`] = {
get: {
tags: [tag],
summary: "예상 택시 요금 반환",
description:
"start, goal, time에 따라 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다. <br/> 카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다.",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
from: { type: "string", pattern: objectIdPattern },
to: { type: "string", pattern: objectIdPattern },
time: { type: "string", format: "date-time" },
},
},
},
},
},
responses: {
200: {
description: "예상 택시 요금 반환 성공",
content: {
"application/json": {
schema: {
type: "object",
properties: {
fare: { type: "number", example: 10000 },
},
},
},
},
},
500: {
description: "fare/getTaxiFareHandler: Failed to load taxi fare",
content: {
"text/html": {
example: "fare/getTaxiFareHandler: Failed to load taxi fare",
},
},
},
},
},
};

module.exports = fareDocs;
14 changes: 14 additions & 0 deletions src/routes/docs/schemas/fareSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { z } = require("zod");
const { zodToSchemaObject } = require("../utils");
const { objectId } = require("../../../modules/patterns");

const fareZod = {
getTaxiFareHandler: z.object({
from: z.string().regex(objectId),
to: z.string().regex(objectId),
time: z.string().datetime(),
}),
};
const fareSchema = zodToSchemaObject(fareZod);

module.exports = { fareSchema, fareZod };
Loading

0 comments on commit 147fc5c

Please sign in to comment.