브라우징 자동화에만 집중

This commit is contained in:
LHK
2025-10-14 17:58:58 +09:00
부모 51deb07fab
커밋 226eede3bf
25개의 변경된 파일5282개의 추가작업 그리고 3006개의 파일을 삭제

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()
});