브라우징 자동화에만 집중
This commit is contained in:
204
backup/attendance.js
Normal file
204
backup/attendance.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const { TesseractWorker } = require('tesseract.js');
|
||||
const Jimp = require('jimp');
|
||||
const db = require('./db');
|
||||
const path = require('path');
|
||||
|
||||
const MAX_RUN_RETRIES = 5;
|
||||
const MAX_ATTEND_RETRIES = 20;
|
||||
|
||||
class Attendance {
|
||||
constructor(accountData) {
|
||||
this.account = accountData;
|
||||
this.browser = null;
|
||||
this.page = null;
|
||||
}
|
||||
|
||||
getISOTime() {
|
||||
return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
async log(message) {
|
||||
const logMessage = `[${this.account.DOMAIN_ACCNT_ID}] ${message}`;
|
||||
console.log(logMessage);
|
||||
await db.run(
|
||||
`INSERT INTO ATNDNC_LOG (DOMAIN_SEQ_ID, DOMAIN_ACCNT_ID, ATNDNC_DT, LOG_MSG, LOG_DTTM) VALUES (?, ?, date('now', 'localtime'), ?, ?)` ,
|
||||
[this.account.DOMAIN_SEQ_ID, this.account.DOMAIN_ACCNT_ID, message, this.getISOTime()]
|
||||
);
|
||||
}
|
||||
|
||||
calculateNextStartTime() {
|
||||
const { MESRMNT_STRT_TM, MESRMNT_END_TM, MESRMNT_TM_INCLSN_PRBLTY } = this.account;
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1); // 내일 날짜
|
||||
|
||||
let hour;
|
||||
const randomPercent = Math.random() * 100;
|
||||
|
||||
if (randomPercent < MESRMNT_TM_INCLSN_PRBLTY) {
|
||||
// 지정된 시간 범위 내에서 랜덤 시간 생성
|
||||
hour = Math.floor(Math.random() * (MESRMNT_END_TM - MESRMNT_STRT_TM + 1)) + MESRMNT_STRT_TM;
|
||||
} else {
|
||||
// 지정된 시간 범위 밖에서 랜덤 시간 생성
|
||||
const isBefore = Math.random() < 0.5;
|
||||
if (isBefore) {
|
||||
hour = Math.floor(Math.random() * MESRMNT_STRT_TM);
|
||||
} else {
|
||||
hour = Math.floor(Math.random() * (24 - MESRMNT_END_TM)) + MESRMNT_END_TM;
|
||||
}
|
||||
}
|
||||
const minute = Math.floor(Math.random() * 60);
|
||||
|
||||
now.setHours(hour, minute, 0, 0);
|
||||
return now.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
async setStatus(status, isSuccess = false) {
|
||||
if (isSuccess) {
|
||||
const nextTime = this.calculateNextStartTime();
|
||||
await db.run(
|
||||
"UPDATE DOMAIN_ACCNT_LIST SET ATNDNC_STTS_CD = ?, LAST_ATNDNC_DTTM = ?, ATNDNC_STRT_DTTM = ? WHERE DOMAIN_SEQ_ID = ? AND DOMAIN_ACCNT_ID = ?",
|
||||
[status, this.getISOTime(), nextTime, this.account.DOMAIN_SEQ_ID, this.account.DOMAIN_ACCNT_ID]
|
||||
);
|
||||
} else {
|
||||
await db.run(
|
||||
"UPDATE DOMAIN_ACCNT_LIST SET ATNDNC_STTS_CD = ? WHERE DOMAIN_SEQ_ID = ? AND DOMAIN_ACCNT_ID = ?",
|
||||
[status, this.account.DOMAIN_SEQ_ID, this.account.DOMAIN_ACCNT_ID]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async run() {
|
||||
let runSuccess = false;
|
||||
let tryCount = 0;
|
||||
|
||||
while (!runSuccess && tryCount < MAX_RUN_RETRIES) {
|
||||
if (tryCount > 0) {
|
||||
await this.log(`${tryCount}번째 전체 재시도를 시작합니다...`);
|
||||
const delay = Math.floor(Math.random() * 10000) + 1000;
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
tryCount++;
|
||||
|
||||
const dialogListener = async (dialog) => {
|
||||
await this.log(`팝업창 발견: "${dialog.message()}". 자동으로 '확인'을 누릅니다.`);
|
||||
await dialog.accept();
|
||||
};
|
||||
|
||||
try {
|
||||
this.browser = await puppeteer.launch({ headless: "new", args: ['--no-sandbox', '--disable-setuid-sandbox'] });
|
||||
this.page = await this.browser.newPage();
|
||||
this.page.on('dialog', dialogListener);
|
||||
|
||||
await this.page.setViewport({ width: 1920, height: 1080 });
|
||||
await this.page.setUserAgent(this.account.USER_AGENT);
|
||||
await this.page.evaluateOnNewDocument(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||
});
|
||||
|
||||
const normalActions = await db.query("SELECT * FROM ACT_LIST WHERE DOMAIN_SEQ_ID = ? AND RETRY_YN = 'N' ORDER BY ACT_ORD", [this.account.DOMAIN_SEQ_ID]);
|
||||
const retryActions = await db.query("SELECT * FROM ACT_LIST WHERE DOMAIN_SEQ_ID = ? AND RETRY_YN = 'Y' ORDER BY ACT_ORD", [this.account.DOMAIN_SEQ_ID]);
|
||||
|
||||
for (const action of normalActions) await this.runAction(action);
|
||||
|
||||
if (retryActions.length > 0) {
|
||||
let attendSuccess = false;
|
||||
let attendTryCount = 0;
|
||||
while (!attendSuccess && attendTryCount < MAX_ATTEND_RETRIES) {
|
||||
if (attendTryCount > 0) await this.log(`출석 액션 ${attendTryCount}번째 재시도를 시작합니다...`);
|
||||
attendTryCount++;
|
||||
|
||||
try {
|
||||
for (const action of retryActions) await this.runAction(action);
|
||||
attendSuccess = true;
|
||||
} catch (error) {
|
||||
await this.log(`출석 액션 실패: ${error.message}`);
|
||||
if (attendTryCount >= MAX_ATTEND_RETRIES) throw new Error('출석 액션 재시도 횟수를 초과했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.log('모든 행동을 성공적으로 완료했습니다.');
|
||||
await this.setStatus('1', true); // 성공 상태(1) 및 다음 시간 설정
|
||||
runSuccess = true;
|
||||
|
||||
} catch (error) {
|
||||
await this.log(`전체 작업 ${tryCount}차 시도 실패: ${error.message}`);
|
||||
if (tryCount >= MAX_RUN_RETRIES) {
|
||||
await this.log('최대 재시도 횟수를 초과하여 작업을 중단합니다.');
|
||||
await this.setStatus('9'); // 최종 실패 상태(9) 설정
|
||||
}
|
||||
} finally {
|
||||
if (this.page) this.page.removeListener('dialog', dialogListener);
|
||||
if (this.browser) await this.browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async runAction(action) {
|
||||
const details = JSON.parse(action.ACT_DTL_JSON);
|
||||
await this.log(`[액션: ${action.ACT_TYP_CD}] 시작`);
|
||||
const xpathSelector = (xpath) => `xpath/${xpath}`;
|
||||
|
||||
switch (action.ACT_TYP_CD) {
|
||||
case 'move':
|
||||
await this.page.goto(this.account.DOMAIN_ADDRS + details.location, { waitUntil: 'networkidle2' });
|
||||
break;
|
||||
case 'input':
|
||||
await this.page.waitForSelector(xpathSelector(details.xpath));
|
||||
const value = details.column ? this.account[details.column] : details.value;
|
||||
await this.page.type(xpathSelector(details.xpath), value);
|
||||
break;
|
||||
case 'click':
|
||||
await this.page.waitForSelector(xpathSelector(details.xpath));
|
||||
await this.page.click(xpathSelector(details.xpath));
|
||||
break;
|
||||
case 'if_captcha':
|
||||
const el = await this.page.$(xpathSelector(details.img_xpath));
|
||||
if (el) await this.solveCaptcha(details);
|
||||
else await this.log('CAPTCHA가 없어 건너뜁니다.');
|
||||
break;
|
||||
case 'confirm':
|
||||
await this.log('팝업창 확인(confirm) 액션을 대기합니다.');
|
||||
await this.page.waitForTimeout(1000);
|
||||
break;
|
||||
case 'exec':
|
||||
await this.page.evaluate(details.script);
|
||||
break;
|
||||
case 'sleep':
|
||||
await this.page.waitForTimeout(parseInt(details.sec, 10) * 1000);
|
||||
break;
|
||||
}
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async solveCaptcha(details) {
|
||||
await this.log('CAPTCHA 해결을 시작합니다...');
|
||||
const xpath = (path) => `xpath/${path}`;
|
||||
const el = await this.page.$(xpath(details.img_xpath));
|
||||
if (!el) throw new Error('CAPTCHA 이미지를 찾을 수 없습니다.');
|
||||
const buffer = await el.screenshot();
|
||||
const image = await Jimp.read(buffer);
|
||||
image.grayscale().contrast(1);
|
||||
const pBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
||||
const worker = await Tesseract.createWorker('eng');
|
||||
const { data: { text } } = await worker.recognize(pBuffer);
|
||||
const ocrText = text.replace(/[^a-zA-Z0-9]/g, '');
|
||||
await this.log(`OCR 결과: ${ocrText}`);
|
||||
await worker.terminate();
|
||||
|
||||
await this.page.type(xpath(details.input_xpath), ocrText);
|
||||
await this.page.click(xpath(details.click_xpath));
|
||||
|
||||
// 캡차 성공 여부 검증
|
||||
if (details.complate_msg) {
|
||||
const result = await this.page.waitForEvent('dialog');
|
||||
if (result.message() !== details.complate_msg) {
|
||||
throw new Error("Captcha Error: 확인 메시지가 일치하지 않습니다.");
|
||||
}
|
||||
await result.accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Attendance;
|
||||
Reference in New Issue
Block a user