브라우징 자동화에만 집중
This commit is contained in:
267
index.js
267
index.js
@@ -1,62 +1,221 @@
|
||||
#!/usr/bin/env node
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cron = require('node-cron');
|
||||
const db = require('./db');
|
||||
const { runBot } = require('./bot');
|
||||
const { chromium, devices } = require('playwright');
|
||||
const db = require('./utils/db');
|
||||
const AESCipher = require('./utils/crypto');
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
// CLI 인자(argument)를 받기 위해 process.argv를 사용할 수 있습니다.
|
||||
const args = process.argv.slice(2);
|
||||
class Main {
|
||||
// 스크립트 전역에서 사용할 상태를 정적 속성으로 선언합니다.
|
||||
static browser = null;
|
||||
static context = null;
|
||||
static page = null;
|
||||
static domainInfo = null;
|
||||
static normalActions = [];
|
||||
static retryActions = [];
|
||||
static account = null;
|
||||
static password = null;
|
||||
static decrypt = null;
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
/**
|
||||
* 스크립트 실행에 필요한 데이터를 로드하고 브라우저를 초기화합니다.
|
||||
*/
|
||||
static async init(args) {
|
||||
console.log('[초기화 시작]');
|
||||
if (!args || args.length === 0) {
|
||||
throw new Error("처리할 DOMAIN_SEQ_ID 인자가 없습니다.");
|
||||
}
|
||||
Main.account = args[1];
|
||||
Main.password = args[2];
|
||||
Main.decrypt = args[3]||'Y';
|
||||
Main.domainInfo = (await db.query("SELECT * FROM DOMAIN_LIST WHERE DOMAIN_SEQ_ID = ?", args[0]))[0];
|
||||
Main.normalActions = await db.query("SELECT * FROM ACT_LIST WHERE DOMAIN_SEQ_ID = ? AND RETRY_YN = 'N' ORDER BY ACT_ORD", [Main.domainInfo.DOMAIN_SEQ_ID]);
|
||||
Main.retryActions = await db.query("SELECT * FROM ACT_LIST WHERE DOMAIN_SEQ_ID = ? AND RETRY_YN = 'Y' ORDER BY ACT_ORD", [Main.domainInfo.DOMAIN_SEQ_ID]);
|
||||
|
||||
console.log(Main.domainInfo);
|
||||
console.log(Main.normalActions);
|
||||
console.log(Main.retryActions);
|
||||
|
||||
|
||||
// ### 1. 크론 스케줄러 설정 ###
|
||||
cron.schedule('*/5 * * * *', () => {
|
||||
console.log(`[CRON] 스케줄된 작업을 실행합니다...`);
|
||||
runBot();
|
||||
});
|
||||
|
||||
|
||||
// ### 2. 웹 서버 API (라우트) 설정 ###
|
||||
app.get('/', async (req, res) => {
|
||||
try {
|
||||
const accounts = await db.query(`
|
||||
SELECT dal.*, dl.DOMAIN_NM
|
||||
FROM DOMAIN_ACCNT_LIST dal
|
||||
JOIN DOMAIN_LIST dl ON dal.DOMAIN_SEQ_ID = dl.DOMAIN_SEQ_ID
|
||||
ORDER BY dal.DOMAIN_SEQ_ID, dal.DOMAIN_ACCNT_ID
|
||||
`);
|
||||
let html = `<h1>출석 계정 목록</h1>
|
||||
<table border="1" style="width:100%; border-collapse: collapse;">
|
||||
<tr style="background-color:#f2f2f2;">
|
||||
<th style="padding: 8px;">사이트</th>
|
||||
<th style="padding: 8px;">계정 ID</th>
|
||||
<th style="padding: 8px;">상태</th>
|
||||
<th style="padding: 8px;">다음 실행</th>
|
||||
<th style="padding: 8px;">사용여부</th>
|
||||
</tr>`;
|
||||
accounts.forEach(acc => {
|
||||
html += `<tr>
|
||||
<td style="padding: 8px;">${acc.DOMAIN_NM}</td>
|
||||
<td style="padding: 8px;">${acc.DOMAIN_ACCNT_ID}</td>
|
||||
<td style="padding: 8px;">${acc.ATNDNC_STTS_CD}</td>
|
||||
<td style="padding: 8px;">${acc.ATNDNC_STRT_DTTM}</td>
|
||||
<td style="padding: 8px;">${acc.USE_YN}</td>
|
||||
</tr>`;
|
||||
// Playwright 브라우저 초기화
|
||||
Main.browser = await chromium.launch({ headless: false });
|
||||
Main.context = await Main.browser.newContext();
|
||||
Main.page = await Main.context.newPage();
|
||||
await Main.page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => false,
|
||||
});
|
||||
});
|
||||
html += '</table>';
|
||||
res.send(html);
|
||||
} catch (error) {
|
||||
res.status(500).send("데이터 조회 중 오류 발생: " + error.message);
|
||||
console.log('[초기화 완료]');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 정의된 액션 목록을 순차적으로 실행합니다.
|
||||
*/
|
||||
static async run() {
|
||||
console.log('[일반 작업 실행 시작]');
|
||||
for (const actData of Main.normalActions) {
|
||||
await Main.runAction(actData);
|
||||
}
|
||||
console.log('[일반 작업 실행 완료]');
|
||||
|
||||
// ### 3. 웹 서버 실행 ###
|
||||
app.listen(PORT, () => {
|
||||
console.log(`웹 서버가 http://localhost:${PORT} 에서 실행 중입니다. (외부 접속: http://localhost:8080)`);
|
||||
console.log('자동 출석 봇이 스케줄 대기 중입니다...');
|
||||
// 서버 시작 시 1회 즉시 실행 (테스트용)
|
||||
runBot();
|
||||
});
|
||||
console.log('[재시도 작업 실행 시작]');
|
||||
for (const actData of Main.retryActions) {
|
||||
await Main.runAction(actData);
|
||||
}
|
||||
console.log('[재시도 작업 실행 완료]');
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 액션을 타입에 따라 분기하여 실행합니다.
|
||||
* @param {object} actData - 실행할 액션의 데이터
|
||||
*/
|
||||
static async runAction(actData) {
|
||||
const actDtlJson = JSON.parse(actData.ACT_DTL_JSON);
|
||||
const actTypCd = actData.ACT_TYP_CD;
|
||||
console.log(`-- 액션 실행: ${actTypCd}, 데이터: ${JSON.stringify(actDtlJson)}`);
|
||||
|
||||
switch (actTypCd) {
|
||||
case 'login':
|
||||
await Main.login(actDtlJson);
|
||||
break;
|
||||
case 'move':
|
||||
await Main.moveUrl(actDtlJson);
|
||||
break;
|
||||
case 'input':
|
||||
await Main.inputValue(actDtlJson);
|
||||
break;
|
||||
case 'click':
|
||||
await Main.clickElement(actDtlJson);
|
||||
break;
|
||||
case 'confirm':
|
||||
await Main.confirmAlert(actDtlJson);
|
||||
break;
|
||||
case 'captcha':
|
||||
await Main.passCaptcha(actDtlJson);
|
||||
break;
|
||||
case 'if_captcha':
|
||||
await Main.ifPassCaptcha(actDtlJson);
|
||||
break;
|
||||
case 'exec':
|
||||
await Main.execScript(actDtlJson);
|
||||
break;
|
||||
case 'sleep':
|
||||
await Main.sleepSec(actDtlJson);
|
||||
break;
|
||||
default:
|
||||
console.log(`[경고] 정의되지 않은 액션 타입입니다: ${actTypCd}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 개별 Action 함수들 ---
|
||||
static async login(actDtlJson) {
|
||||
console.log(' [login] 로그인 처리 시작');
|
||||
console.log(` -> 계정: ${Main.account}, 비번: ${Main.password ? '********' : '(없음)'}`);
|
||||
if (!Main.account || !Main.password) {
|
||||
throw new Error("로그인시 비번 패스 필수");
|
||||
}
|
||||
actDtlJson.value = Main.account;
|
||||
await Main.inputValue(actDtlJson, 'id_xpath');
|
||||
actDtlJson.value = Main.password;
|
||||
actDtlJson.decrypt = Main.decrypt;
|
||||
await Main.inputValue(actDtlJson, 'pw_xpath');
|
||||
await Main.clickElement(actDtlJson, 'login_xpath');
|
||||
console.log(' -> 로그인 완료');
|
||||
}
|
||||
|
||||
static async moveUrl(actDtlJson) {
|
||||
const url = Main.domainInfo.DOMAIN_ADDRS + actDtlJson.location;
|
||||
console.log(` [moveUrl] ${url}로 이동`);
|
||||
await Main.page.goto(url);
|
||||
}
|
||||
|
||||
static async inputValue(actDtlJson, path = 'xpath') {
|
||||
const xpath = actDtlJson[path];
|
||||
let value = actDtlJson.value || Main.domainInfo[actDtlJson.column];
|
||||
const logValue = value; // 로그에는 복호화 전 값을 남길 수 있음
|
||||
|
||||
if (actDtlJson.decrypt === 'Y') {
|
||||
value = AESCipher.decrypt(value); // 복호화
|
||||
}
|
||||
|
||||
console.log(` [inputValue] '${xpath}'에 값 '${logValue}' 입력`);
|
||||
await Main.page.locator(`xpath=${xpath}`).fill(value);
|
||||
}
|
||||
|
||||
static async clickElement(actDtlJson, path = 'xpath') {
|
||||
const xpath = actDtlJson[path];
|
||||
console.log(` [clickElement] '${xpath}' 클릭`);
|
||||
await Main.page.locator(`xpath=${xpath}`).click();
|
||||
}
|
||||
|
||||
static async confirmAlert(actDtlJson) {
|
||||
console.log(' [confirmAlert] 다음 액션에서 발생할 Alert/Confirm 창을 자동으로 수락하도록 설정');
|
||||
// 'dialog' 이벤트 핸들러를 '한 번만' 등록합니다.
|
||||
// 이 코드가 실행된 후 alert를 발생시키는 클릭 등의 액션이 와야 합니다.
|
||||
Main.page.once('dialog', dialog => {
|
||||
console.log(` -> Alert 창 발견: "${dialog.message()}", 수락합니다.`);
|
||||
dialog.accept();
|
||||
});
|
||||
}
|
||||
|
||||
static async passCaptcha(actDtlJson) {
|
||||
console.log(' [passCaptcha] 캡차 처리 시작');
|
||||
const imgLocator = Main.page.locator(`xpath=${actDtlJson.img_xpath}`);
|
||||
|
||||
const screenshotBuffer = await imgLocator.screenshot();
|
||||
const text = await ocr.processImage(screenshotBuffer); // 별도 OCR 모듈 호출
|
||||
|
||||
if (!text || text.length === 0) {
|
||||
throw new Error("Captcha OCR 실패: 텍스트를 인식할 수 없습니다.");
|
||||
}
|
||||
console.log(` -> OCR 인식 결과: ${text}`);
|
||||
|
||||
await Main.page.locator(`xpath=${actDtlJson.input_xpath}`).fill(text);
|
||||
await Main.page.locator(`xpath=${actDtlJson.click_xpath}`).click();
|
||||
console.log(' -> 캡차 값 입력 및 확인 완료');
|
||||
}
|
||||
|
||||
static async ifPassCaptcha(actDtlJson) {
|
||||
console.log(' [ifPassCaptcha] 캡차 존재 여부 확인');
|
||||
const isCaptchaVisible = await Main.page.locator(`xpath=${actDtlJson.img_xpath}`).isVisible();
|
||||
|
||||
if (isCaptchaVisible) {
|
||||
console.log(' -> 캡차 발견. 처리를 시작합니다.');
|
||||
await Main.passCaptcha(actDtlJson);
|
||||
} else {
|
||||
console.log(' -> 캡차 없음. 다음 단계로 진행합니다.');
|
||||
}
|
||||
}
|
||||
|
||||
static async execScript(actDtlJson) {
|
||||
const script = actDtlJson.script;
|
||||
console.log(` [execScript] 스크립트 실행: ${script}`);
|
||||
await Main.page.evaluate(script); // page.evaluate를 사용하여 브라우저 컨텍스트에서 스크립트 실행
|
||||
}
|
||||
|
||||
static async sleepSec(actDtlJson) {
|
||||
const sec = parseInt(actDtlJson.sec, 10);
|
||||
console.log(` [sleepSec] ${sec}초 대기`);
|
||||
await Main.page.waitForTimeout(sec * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// === 스크립트 실행 부분 ===
|
||||
(async () => {
|
||||
try {
|
||||
// new 키워드 없이 클래스에서 직접 메서드를 호출합니다.
|
||||
await Main.init(args);
|
||||
await Main.run();
|
||||
console.log('[성공] 모든 작업이 성공적으로 완료되었습니다.');
|
||||
} catch (error) {
|
||||
console.error("[오류] 스크립트 실행 중 오류가 발생했습니다:", error);
|
||||
} finally {
|
||||
// 스크립트가 성공하든 실패하든 항상 브라우저를 닫습니다.
|
||||
if (Main.browser) {
|
||||
await Main.browser.close();
|
||||
console.log('[종료] 브라우저를 닫았습니다.');
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user