브라우징 자동화에만 집중
This commit is contained in:
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,9 +1,8 @@
|
|||||||
|
|
||||||
|
# Playwright
|
||||||
node_modules/
|
node_modules/
|
||||||
.env
|
/test-results/
|
||||||
data/database.sqlite
|
/playwright-report/
|
||||||
*.log
|
/blob-report/
|
||||||
npm-debug.log*
|
/playwright/.cache/
|
||||||
.DS_Store
|
/playwright/.auth/
|
||||||
Thumbs.db
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
|
|||||||
9
backup/.gitignore
vendored
Normal file
9
backup/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
data/database.sqlite
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
62
backup/index.js
Normal file
62
backup/index.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const express = require('express');
|
||||||
|
const cron = require('node-cron');
|
||||||
|
const db = require('./db');
|
||||||
|
const { runBot } = require('./bot');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
|
||||||
|
// ### 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>`;
|
||||||
|
});
|
||||||
|
html += '</table>';
|
||||||
|
res.send(html);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send("데이터 조회 중 오류 발생: " + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ### 3. 웹 서버 실행 ###
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`웹 서버가 http://localhost:${PORT} 에서 실행 중입니다. (외부 접속: http://localhost:8080)`);
|
||||||
|
console.log('자동 출석 봇이 스케줄 대기 중입니다...');
|
||||||
|
// 서버 시작 시 1회 즉시 실행 (테스트용)
|
||||||
|
runBot();
|
||||||
|
});
|
||||||
4280
backup/package-lock.json
generated
Normal file
4280
backup/package-lock.json
generated
Normal file
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
23
backup/package.json
Normal file
23
backup/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "auto-browse",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"fake-useragent": "^1.0.1",
|
||||||
|
"jimp": "^1.6.0",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
|
"puppeteer": "^24.24.0",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"tesseract.js": "^6.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
107
backup/setupDatabase.js
Normal file
107
backup/setupDatabase.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const fs = require('fs'); // 1. 파일 시스템 모듈 가져오기
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const dbPath = path.resolve(__dirname, 'data', 'database.sqlite');
|
||||||
|
|
||||||
|
// 2. 데이터베이스 파일이 이미 존재하는지 확인
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
console.log(`데이터베이스 파일이 이미 존재합니다. 초기화를 건너뜁니다: ${dbPath}`);
|
||||||
|
// 파일이 있으면 스크립트를 여기서 종료
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 아래 코드는 파일이 없을 때만 실행됩니다 ---
|
||||||
|
|
||||||
|
console.log("새로운 데이터베이스 파일을 생성하고 초기화를 시작합니다...");
|
||||||
|
const db = new sqlite3.Database(dbPath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("데이터베이스 연결 실패:", err.message);
|
||||||
|
} else {
|
||||||
|
console.log(`SQLite 데이터베이스에 성공적으로 연결되었습니다: ${dbPath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 테이블 생성을 순차적으로 실행
|
||||||
|
db.serialize(() => {
|
||||||
|
console.log("테이블 생성을 시작합니다...");
|
||||||
|
|
||||||
|
// 1. DOMAIN_LIST
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE DOMAIN_LIST (
|
||||||
|
DOMAIN_SEQ_ID INTEGER PRIMARY KEY,
|
||||||
|
DOMAIN_NM TEXT,
|
||||||
|
DOMAIN_ADDRS TEXT,
|
||||||
|
DOMAIN_DESC TEXT,
|
||||||
|
USE_YN TEXT DEFAULT 'Y',
|
||||||
|
RGST_DTTM TEXT
|
||||||
|
)
|
||||||
|
`, (err) => {
|
||||||
|
if (err) console.error("DOMAIN_LIST 테이블 생성 오류:", err.message);
|
||||||
|
else console.log("DOMAIN_LIST 테이블이 성공적으로 생성되었습니다.");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. DOMAIN_ACCNT_LIST
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE DOMAIN_ACCNT_LIST (
|
||||||
|
DOMAIN_SEQ_ID INTEGER NOT NULL,
|
||||||
|
DOMAIN_ACCNT_ID TEXT NOT NULL,
|
||||||
|
DOMAIN_ACCNT_PSWRD TEXT,
|
||||||
|
LAST_ATNDNC_DTTM TEXT,
|
||||||
|
MESRMNT_STRT_TM INTEGER DEFAULT 8,
|
||||||
|
MESRMNT_END_TM INTEGER DEFAULT 20,
|
||||||
|
MESRMNT_TM_INCLSN_PRBLTY INTEGER DEFAULT 95,
|
||||||
|
ATNDNC_STRT_DTTM TEXT,
|
||||||
|
ATNDNC_STTS_CD TEXT,
|
||||||
|
DOMAIN_ACCNT_DESC TEXT,
|
||||||
|
USE_YN TEXT DEFAULT 'Y',
|
||||||
|
RGST_DTTM TEXT,
|
||||||
|
USER_AGENT TEXT,
|
||||||
|
PRIMARY KEY (DOMAIN_SEQ_ID, DOMAIN_ACCNT_ID)
|
||||||
|
)
|
||||||
|
`, (err) => {
|
||||||
|
if (err) console.error("DOMAIN_ACCNT_LIST 테이블 생성 오류:", err.message);
|
||||||
|
else console.log("DOMAIN_ACCNT_LIST 테이블이 성공적으로 생성되었습니다.");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. ACT_LIST
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE ACT_LIST (
|
||||||
|
DOMAIN_SEQ_ID INTEGER NOT NULL,
|
||||||
|
ACT_ORD INTEGER NOT NULL,
|
||||||
|
ACT_TYP_CD TEXT,
|
||||||
|
ACT_DTL_JSON TEXT CHECK(json_valid(ACT_DTL_JSON)),
|
||||||
|
RETRY_YN TEXT,
|
||||||
|
RGST_DTTM TEXT,
|
||||||
|
PRIMARY KEY (DOMAIN_SEQ_ID, ACT_ORD)
|
||||||
|
)
|
||||||
|
`, (err) => {
|
||||||
|
if (err) console.error("ACT_LIST 테이블 생성 오류:", err.message);
|
||||||
|
else console.log("ACT_LIST 테이블이 성공적으로 생성되었습니다.");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. ATNDNC_LOG
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE ATNDNC_LOG (
|
||||||
|
DOMAIN_SEQ_ID INTEGER NOT NULL,
|
||||||
|
DOMAIN_ACCNT_ID TEXT NOT NULL,
|
||||||
|
ATNDNC_DT TEXT NOT NULL,
|
||||||
|
LOG_SEQ_ID INTEGER NOT NULL,
|
||||||
|
LOG_MSG TEXT,
|
||||||
|
LOG_DTTM TEXT,
|
||||||
|
PRIMARY KEY (DOMAIN_SEQ_ID, DOMAIN_ACCNT_ID, ATNDNC_DT, LOG_SEQ_ID)
|
||||||
|
)
|
||||||
|
`, (err) => {
|
||||||
|
if (err) console.error("ATNDNC_LOG 테이블 생성 오류:", err.message);
|
||||||
|
else console.log("ATNDNC_LOG 테이블이 성공적으로 생성되었습니다.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 데이터베이스 연결 종료
|
||||||
|
db.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("데이터베이스 연결 종료 실패:", err.message);
|
||||||
|
} else {
|
||||||
|
console.log("데이터베이스 초기화 및 연결 종료가 완료되었습니다.");
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
data/database.sqlite
Normal file
BIN
data/database.sqlite
Normal file
Binary file not shown.
265
index.js
265
index.js
@@ -1,62 +1,221 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const { chromium, devices } = require('playwright');
|
||||||
const cron = require('node-cron');
|
const db = require('./utils/db');
|
||||||
const db = require('./db');
|
const AESCipher = require('./utils/crypto');
|
||||||
const { runBot } = require('./bot');
|
|
||||||
|
|
||||||
const app = express();
|
// CLI 인자(argument)를 받기 위해 process.argv를 사용할 수 있습니다.
|
||||||
const PORT = 3000;
|
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. 크론 스케줄러 설정 ###
|
// Playwright 브라우저 초기화
|
||||||
cron.schedule('*/5 * * * *', () => {
|
Main.browser = await chromium.launch({ headless: false });
|
||||||
console.log(`[CRON] 스케줄된 작업을 실행합니다...`);
|
Main.context = await Main.browser.newContext();
|
||||||
runBot();
|
Main.page = await Main.context.newPage();
|
||||||
});
|
await Main.page.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, 'webdriver', {
|
||||||
|
get: () => false,
|
||||||
// ### 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>`;
|
|
||||||
});
|
});
|
||||||
html += '</table>';
|
console.log('[초기화 완료]');
|
||||||
res.send(html);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send("데이터 조회 중 오류 발생: " + error.message);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정의된 액션 목록을 순차적으로 실행합니다.
|
||||||
|
*/
|
||||||
|
static async run() {
|
||||||
|
console.log('[일반 작업 실행 시작]');
|
||||||
|
for (const actData of Main.normalActions) {
|
||||||
|
await Main.runAction(actData);
|
||||||
|
}
|
||||||
|
console.log('[일반 작업 실행 완료]');
|
||||||
|
|
||||||
// ### 3. 웹 서버 실행 ###
|
console.log('[재시도 작업 실행 시작]');
|
||||||
app.listen(PORT, () => {
|
for (const actData of Main.retryActions) {
|
||||||
console.log(`웹 서버가 http://localhost:${PORT} 에서 실행 중입니다. (외부 접속: http://localhost:8080)`);
|
await Main.runAction(actData);
|
||||||
console.log('자동 출석 봇이 스케줄 대기 중입니다...');
|
}
|
||||||
// 서버 시작 시 1회 즉시 실행 (테스트용)
|
console.log('[재시도 작업 실행 완료]');
|
||||||
runBot();
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* 단일 액션을 타입에 따라 분기하여 실행합니다.
|
||||||
|
* @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('[종료] 브라우저를 닫았습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
3062
package-lock.json
generated
3062
package-lock.json
generated
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
24
package.json
24
package.json
@@ -1,23 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "auto-browse",
|
"name": "web-sailor",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {},
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@git.lhk.o-r.kr:simple-utils/simple-auto_browsing.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.56.0",
|
||||||
|
"@types/node": "^24.7.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sail": "./index.js"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^2.2.0",
|
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.19.2",
|
"sqlite3": "^5.1.7"
|
||||||
"fake-useragent": "^1.0.1",
|
|
||||||
"jimp": "^1.6.0",
|
|
||||||
"node-cron": "^4.2.1",
|
|
||||||
"puppeteer": "^24.24.0",
|
|
||||||
"sqlite3": "^5.1.7",
|
|
||||||
"tesseract.js": "^6.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
playwright.config.js
Normal file
104
playwright.config.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('')`. */
|
||||||
|
// baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'Desktop Edge',
|
||||||
|
use: {
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 'chrome'은 PC에 설치된 실제 Chrome 브라우저를 사용하라는 의미입니다.
|
||||||
|
// 'msedge' (Edge), 'chrome-beta' 등도 사용 가능합니다.
|
||||||
|
channel: 'msedge',
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
|
// 헤드리스 모드를 끄는 것도 탐지 우회에 도움이 됩니다.
|
||||||
|
headless: false,
|
||||||
|
|
||||||
|
...devices['Desktop Edge'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Desktop Chrome',
|
||||||
|
use: {
|
||||||
|
channel: 'chrome',
|
||||||
|
headless: false,
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: 'chromium',
|
||||||
|
// use: { ...devices['Desktop Chrome'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: 'firefox',
|
||||||
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: 'webkit',
|
||||||
|
// use: { ...devices['Desktop Safari'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: 'npm run start',
|
||||||
|
// url: 'http://localhost:3000',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
@@ -41,29 +41,6 @@ db.serialize(() => {
|
|||||||
else console.log("DOMAIN_LIST 테이블이 성공적으로 생성되었습니다.");
|
else console.log("DOMAIN_LIST 테이블이 성공적으로 생성되었습니다.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. DOMAIN_ACCNT_LIST
|
|
||||||
db.run(`
|
|
||||||
CREATE TABLE DOMAIN_ACCNT_LIST (
|
|
||||||
DOMAIN_SEQ_ID INTEGER NOT NULL,
|
|
||||||
DOMAIN_ACCNT_ID TEXT NOT NULL,
|
|
||||||
DOMAIN_ACCNT_PSWRD TEXT,
|
|
||||||
LAST_ATNDNC_DTTM TEXT,
|
|
||||||
MESRMNT_STRT_TM INTEGER DEFAULT 8,
|
|
||||||
MESRMNT_END_TM INTEGER DEFAULT 20,
|
|
||||||
MESRMNT_TM_INCLSN_PRBLTY INTEGER DEFAULT 95,
|
|
||||||
ATNDNC_STRT_DTTM TEXT,
|
|
||||||
ATNDNC_STTS_CD TEXT,
|
|
||||||
DOMAIN_ACCNT_DESC TEXT,
|
|
||||||
USE_YN TEXT DEFAULT 'Y',
|
|
||||||
RGST_DTTM TEXT,
|
|
||||||
USER_AGENT TEXT,
|
|
||||||
PRIMARY KEY (DOMAIN_SEQ_ID, DOMAIN_ACCNT_ID)
|
|
||||||
)
|
|
||||||
`, (err) => {
|
|
||||||
if (err) console.error("DOMAIN_ACCNT_LIST 테이블 생성 오류:", err.message);
|
|
||||||
else console.log("DOMAIN_ACCNT_LIST 테이블이 성공적으로 생성되었습니다.");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. ACT_LIST
|
// 3. ACT_LIST
|
||||||
db.run(`
|
db.run(`
|
||||||
CREATE TABLE ACT_LIST (
|
CREATE TABLE ACT_LIST (
|
||||||
@@ -80,21 +57,6 @@ db.serialize(() => {
|
|||||||
else console.log("ACT_LIST 테이블이 성공적으로 생성되었습니다.");
|
else console.log("ACT_LIST 테이블이 성공적으로 생성되었습니다.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. ATNDNC_LOG
|
|
||||||
db.run(`
|
|
||||||
CREATE TABLE ATNDNC_LOG (
|
|
||||||
DOMAIN_SEQ_ID INTEGER NOT NULL,
|
|
||||||
DOMAIN_ACCNT_ID TEXT NOT NULL,
|
|
||||||
ATNDNC_DT TEXT NOT NULL,
|
|
||||||
LOG_SEQ_ID INTEGER NOT NULL,
|
|
||||||
LOG_MSG TEXT,
|
|
||||||
LOG_DTTM TEXT,
|
|
||||||
PRIMARY KEY (DOMAIN_SEQ_ID, DOMAIN_ACCNT_ID, ATNDNC_DT, LOG_SEQ_ID)
|
|
||||||
)
|
|
||||||
`, (err) => {
|
|
||||||
if (err) console.error("ATNDNC_LOG 테이블 생성 오류:", err.message);
|
|
||||||
else console.log("ATNDNC_LOG 테이블이 성공적으로 생성되었습니다.");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 데이터베이스 연결 종료
|
// 데이터베이스 연결 종료
|
||||||
|
|||||||
21
tests/eeee.spec.js
Normal file
21
tests/eeee.spec.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, 'webdriver', {
|
||||||
|
get: () => false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('https://naver.com');
|
||||||
|
await page.goto('https://oname.kr/member/login.html');
|
||||||
|
|
||||||
|
// 브라우저의 콘솔에서 navigator.webdriver 값을 가져옵니다.
|
||||||
|
const webdriverValue = await page.evaluate(() => navigator.webdriver);
|
||||||
|
|
||||||
|
// 콘솔에 값을 출력해봅니다.
|
||||||
|
console.log(`navigator.webdriver 값: ${webdriverValue}`); // 'false'가 출력되어야 합니다.
|
||||||
|
|
||||||
|
// 값이 false인지 단언(assert)하여 테스트를 통과시킵니다.
|
||||||
|
expect(webdriverValue).toBe(false);
|
||||||
|
});
|
||||||
199
tests/test.spec.js
Normal file
199
tests/test.spec.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
const { chromium, devices } = require('playwright');
|
||||||
|
const db = require('../db');
|
||||||
|
const AESCipher = require('../crypto');
|
||||||
|
|
||||||
|
// 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 async init() {
|
||||||
|
console.log('[초기화 시작]');
|
||||||
|
// 'this' 대신 'Main' 클래스 이름으로 정적 속성에 데이터를 할당합니다.
|
||||||
|
|
||||||
|
const limit = process.env.GET_DOMAIN_COUNT || 5;
|
||||||
|
|
||||||
|
Main.domainInfo = (await db.query(`
|
||||||
|
SELECT
|
||||||
|
dl.DOMAIN_ADDRS,
|
||||||
|
dal.*
|
||||||
|
FROM DOMAIN_ACCNT_LIST dal
|
||||||
|
JOIN DOMAIN_LIST dl ON dal.DOMAIN_SEQ_ID = dl.DOMAIN_SEQ_ID
|
||||||
|
WHERE dal.USE_YN = 'Y'
|
||||||
|
AND dl.USE_YN = 'Y'
|
||||||
|
AND dal.ATNDNC_STRT_DTTM < datetime('now', 'localtime')
|
||||||
|
AND dal.ATNDNC_STTS_CD = '1'
|
||||||
|
LIMIT ?
|
||||||
|
`, [limit]))[0];
|
||||||
|
Main.normalActions = await db.query("SELECT * FROM ACT_LIST WHERE DOMAIN_SEQ_ID = 1 AND RETRY_YN = 'N' ORDER BY ACT_ORD");
|
||||||
|
Main.retryActions = await db.query("SELECT * FROM ACT_LIST WHERE DOMAIN_SEQ_ID = 1 AND RETRY_YN = 'Y' ORDER BY ACT_ORD");
|
||||||
|
console.log(Main.domainInfo);
|
||||||
|
console.log(Main.normalActions);
|
||||||
|
console.log(Main.retryActions);
|
||||||
|
|
||||||
|
// Playwright 브라우저 초기화
|
||||||
|
Main.browser = await chromium.launch({ headless: false });
|
||||||
|
Main.context = await Main.browser.newContext();
|
||||||
|
Main.page = await Main.context.newPage();
|
||||||
|
console.log('[초기화 완료]');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정의된 액션 목록을 순차적으로 실행합니다.
|
||||||
|
*/
|
||||||
|
static async run() {
|
||||||
|
console.log('[일반 작업 실행 시작]');
|
||||||
|
for (const actData of Main.normalActions) {
|
||||||
|
await Main.runAction(actData);
|
||||||
|
}
|
||||||
|
console.log('[일반 작업 실행 완료]');
|
||||||
|
|
||||||
|
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 '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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('get started link', async ({ page }) => {
|
||||||
|
|
||||||
|
await Main.init();
|
||||||
|
await Main.run();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('https://naver.com');
|
||||||
|
await page.goto('https://oname.kr/member/login.html');
|
||||||
|
page.pause()
|
||||||
|
});
|
||||||
29
utils/crypto.js
Normal file
29
utils/crypto.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const ALGORITHM = 'aes-256-cbc';
|
||||||
|
const KEY = crypto.createHash('sha256').update(process.env.CRYPT_KEY).digest();
|
||||||
|
const IV = Buffer.alloc(16, 0); // Python의 chr(0) * 16 과 동일
|
||||||
|
|
||||||
|
class AESCipher {
|
||||||
|
static encrypt(text) {
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, KEY, IV);
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static decrypt(encryptedText) {
|
||||||
|
try {
|
||||||
|
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, IV);
|
||||||
|
let decrypted = decipher.update(encryptedText, 'base64', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
return decrypted;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("복호화 실패:", error.message);
|
||||||
|
return null; // 또는 오류 처리
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AESCipher;
|
||||||
44
utils/db.js
Normal file
44
utils/db.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const dbPath = path.resolve(__dirname, '../data', 'database.sqlite');
|
||||||
|
const db = new sqlite3.Database(dbPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise 기반으로 DB 쿼리를 실행합니다.
|
||||||
|
* @param {string} sql - 실행할 SQL 쿼리
|
||||||
|
* @param {Array} params - SQL에 바인딩할 파라미터 배열
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
function query(sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(sql, params, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('DB 쿼리 오류:', sql, params);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
resolve(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 INSERT, UPDATE, DELETE 쿼리를 실행합니다.
|
||||||
|
* @param {string} sql
|
||||||
|
* @param {Array} params
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
function run(sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.run(sql, params, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('DB 실행 오류:', sql, params);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
// this.lastID, this.changes를 resolve 할 수도 있음
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { query, run };
|
||||||
Reference in New Issue
Block a user