#!/usr/bin/env node require('dotenv').config(); const { chromium, devices } = require('playwright'); const db = require('./utils/db'); const AESCipher = require('./utils/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 account = null; static password = null; static decrypt = null; /** * 스크립트 실행에 필요한 데이터를 로드하고 브라우저를 초기화합니다. */ 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); // 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, }); }); 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 '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('[종료] 브라우저를 닫았습니다.'); } } })();