Skip to content

Commit

Permalink
[feat] 마스터키 예약 동작 구현 (#4)
Browse files Browse the repository at this point in the history
* [feat] puppeteer 환경 설정

* [feat] 예약 동작 구현

* [feat] 확인 동작 구현

* [fix] 테스트용 데이터 삭제

* [feat] 요청과 응답 구현
  • Loading branch information
sharkslee0 authored Nov 19, 2023
1 parent 67e3e1b commit d30de8f
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 0 deletions.
24 changes: 24 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import express from "express";
import http from "http";
import bodyParser from "body-parser";
import reserveMasterkey from "./src/masterkey/run.js";

const app = express();
app.use(express.json({ extended: true }));

app.use(bodyParser.urlencoded({ extended: false }));

const server = http.createServer(app);
const PORT = 3000;

app.post("/reserve/masterkey", async (req, res) => {
const info = req.body;
const response = await reserveMasterkey(info);

res.json({
isSucceed: response,
});
});

server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

/**
info 객체 예시
{
"targetUrl": "https://www.master-key.co.kr/booking/bk_detail?bid=11",
"themeTitle": "연애조작단",
"targetDate": "2023-11-25",
"targetTime": "99:05",
"bookerName": "이남곤",
"bookerPhoneNumber": "01012345678"
}
*/
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "index.js",
"type": "module",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
Expand Down
69 changes: 69 additions & 0 deletions src/masterkey/check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createBrowser, createPage } from "../tools/browser.js";

const check = async ({
targetUrl,
themeTitle,
targetDate,
targetTime,
bookerName,
bookerPhoneNumber,
}) => {
const browser = await createBrowser();
const page = await createPage(browser);

try {
await Promise.all([page.waitForNavigation(), page.goto(targetUrl)]);

await Promise.all([
page.waitForSelector(".modal-content", { visible: true }),
page.waitForSelector("input[name='chk_user_name']"),
page.waitForSelector("input[name='chk_user_phone']"),
page.click("a#booking_check_btn"),
]);

await page.type("input[name='chk_user_name']", bookerName);
await page.type("input[name='chk_user_phone']", bookerPhoneNumber);
await page.click("button#booking_check_ajax");

await page.waitForSelector("#booking_check_result > table > tbody");
const [tbody] = await page.$x("//*[@id='booking_check_result']/table/tbody");

const trs = await tbody.$x("//tr");

for (let index = 0; index < trs.length; index += 2) {
const fromSite = {};
fromSite["date"] = await trs[index].$eval("td:nth-child(2)", (el) => el.textContent.trim());
fromSite["themeTitle"] = await trs[index + 1].$eval("td:nth-child(1)", (el) =>
el.textContent.trim()
);
const status = await trs[index + 1].$eval("td:nth-child(2)", (el) => el.textContent.trim());

const fromUser = { themeTitle, targetDate, targetTime };

if (isReserved(fromUser, fromSite, status)) {
return true;
}
}

return false;
} catch (error) {
console.log(error);
return false;
} finally {
await page.close();
await browser.close();
}
};

const isReserved = (fromUser, fromSite, status) => {
if (
fromUser.themeTitle == fromSite.themeTitle &&
`${fromUser.targetDate} ${fromUser.targetTime}` == fromSite.date &&
status == "예약됨"
) {
return true;
}
return false;
};

export default check;
67 changes: 67 additions & 0 deletions src/masterkey/reserve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createBrowser, createPage } from "../tools/browser.js";

const reserve = async ({
targetUrl,
themeTitle,
targetDate,
targetTime,
bookerName,
bookerPhoneNumber,
}) => {
const browser = await createBrowser();
const page = await createPage(browser);

try {
await Promise.all([page.waitForNavigation(), page.goto(targetUrl)]);

// 날짜 클릭, 해당 날짜 없는 경우 에러 발생
await Promise.all([
page.waitForSelector("#booking_list", { visible: true }),
page.click(`#tab1 > div.box1 > div > div.date-click > div > p[data-dd='${targetDate}']`),
]);

// 테마 선택, 해당 테마 없는 경우 에러 발생
const [box2Inner] = await page.$x(
`//div[@class='box2-inner' and descendant::div[@class='title' and contains(., '${themeTitle}')]]`
);

// 시간과 예약 상태 선택
const [aTag] = await box2Inner.$x(`//div[@class='right']/p/a[contains(., '${targetTime}')]`);

// 해당 시간 없는 경우, undefined 반환
if (!aTag) {
return false;
}

const isAvailable = await aTag.$eval("span", (el) => el.textContent.trim());

// 이미 예약완료된 테마인 경우
if (isAvailable == "예약완료") {
return false;
}

await Promise.all([
page.waitForSelector(".modal-content", { visible: true }),
page.waitForSelector("button#booking_go"),
page.waitForSelector("input[name='user_name']"),
page.waitForSelector("input[name='user_phone']"),
aTag.click(),
]);

await page.type("input[name='user_name']", bookerName);
await page.type("input[name='user_phone']", bookerPhoneNumber);

// 예약 버튼 클릭
await page.click("button#booking_go");

return true;
} catch (error) {
console.log(error);
return false;
} finally {
await page.close();
await browser.close();
}
};

export default reserve;
14 changes: 14 additions & 0 deletions src/masterkey/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import check from "./check.js";
import reserve from "./reserve.js";

const run = async (info) => {
const reservationResult = await reserve(info);

if (!reservationResult) {
return false;
}

return await check(info);
};

export default run;

0 comments on commit d30de8f

Please sign in to comment.