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 all 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
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;
ybmin marked this conversation as resolved.
Show resolved Hide resolved

/**
* 출발 시간 (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
Loading