매크로 깃허브
0. 결과물
- 컴퓨터가 켜있고, 데스크탑앱이 실행중이라면 매크로가 매일 오전 12:01에 자동으로 실행함으로 이제 어이없게 챌린지를 실패할일 없음!
1. 챌린지 매크로
ㄱ. 프로젝트 소개
- 100일간 강의를 들으면 100% 환급을 해주는 강의를 듣던중 까먹고 하루를 못들었음
- 이렇게 하다가 99일째에도 까먹고 못들을꺼 같아서 매크로를 만들기로 결심
- Electron, Express, Puppeteer 사용하여 매일 오전 12:01분에 자동으로 강의영상을 틀게끔 매크로를 돌려보자
ㄴ. 사용스택
- Electron : electron을 사용하여 매크로 앱을 만들것
- Express : 매크로 앱이 실행되면 express로 로컬호스트 4000번 포트를 실행하여 간단하게 서버를 구축할것
- Puppeteer : Dom을 직접조작하여 24간마다 홈페이지에 접근하여 로그인을하고 강의영상을 클릭할것
2. 프로젝트 준비
ㄱ. 프로젝트
- 프로젝트 폴더를 하나만들고
npm init -y
하고 필요한 Electron, Express, Puppeteer 등을 설치
ㄴ. 설치
- Electron :
npm install electron --save-dev
- Express :
npm install express
- Puppeteer :
npm install puppeteer
3. Electron & Express
ㄱ. 기본설정
- npm start 하면 Electron 앱이 실행되게 package.json의 scripts를 수정해줌
"scripts": {
"start": "electron ."
}
- main.js파일을 만들고 Electon가 정상작동하는지 테스트해볼것
// main.js
const { app, BrowserWindow } = require('electron');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
- Electon앱이 실행될때 보여줄 index.html파일을 만들기
// index.html
<!DOCTYPE html>
<html>
<head>
<title>My Electron App</title>
</head>
<body>
<h1>Hello World!</h1>
<p>This is my first Electron app.</p>
</body>
</html>
- 기본설정을 마치고
npm start
를 터미널에 입력하면 아래 이미지처럼 잘됨
ㄴ.Html 꾸미기
- Html의 기본적인 요소(제목 ,Input, 현재시간, 로그인버튼)를 꾸며줌
- 로그인 완료후에도 보여질 화면도 꾸며줌
ㄴ.로그인
npm start
를 하면 Electron앱 실행과 동시에 Express가 실행되야함
// main.js
const { app, BrowserWindow } = require('electron');
const express = require('express');
const puppeteer = require('puppeteer');
const serverApp = express();
const port = 4000;
serverApp.get('/', (req, res) => {
res.send('Hello, express!');
});
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
win.loadURL(`http://localhost:${port}`);
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
- Html에 입력한 input의 Id와 Password값을 받아야함
- index. html에 스크립트 파일을 설정해주고 form 태그 id를
loginForm
로 정의함
// html
document
.getElementById("loginForm")
.addEventListener("submit", async function (event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const response = await fetch(form.action, {
method: form.method,
body: new URLSearchParams(formData),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
if (response.ok) {
console.log("response", response);
alert("로그인 완료");
document.getElementById("content").classList.remove("hidden");
document.getElementById("loginForm").classList.add("hidden");
document.getElementById("loginMessage").classList.remove("hidden");
} else {
alert("로그인 실패");
}
});
- index.html에서 보내준 아이디와 비밀번호를 let변수에 저장해둠
//main.js
let username
let password
serverApp.post('/login', (req, res) => {
username = req.body.username;
password = req.body.password;
console.log(`아이디: ${username}, 비밀번호: ${password}`);
res.send('로그인 정보가 전송되었습니다.');
});
ㄷ. 챌린지 강의 듣기
- 이제 로그인을 완료하면 Input form이 없어지게 styles.css를 수정하고
- 아래 이미지의 챌린지 강의듣기 버튼을 매일 오전 12:01일때 main.js로 play라는 값을 보내서 Puppeteer가 작동하게할것
play.js
파일을 만들고 아래 코드를 넣어줌
document.getElementById("play").addEventListener("click", async function () {
const sendPlayRequest = async () => {
const response = await fetch("/play", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ action: "play" }),
});
if (response.ok) {
alert("챌린지 강의 듣기 시작");
} else {
alert("챌린지 강의 듣기 실패");
}
};
await sendPlayRequest();
// 특정 시간이 될 때까지 체크하는 함수
const checkTimeAndSendRequest = () => {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
// 오전 12시 1분에 요청 보내기
if (hours === 0 && minutes === 1) {
sendPlayRequest();
clearInterval(intervalId); // 요청을 보낸 후 interval을 중지합니다.
}
};
// 1초마다 현재 시간을 체크합니다.
const intervalId = setInterval(checkTimeAndSendRequest, 1000);
});
- setTimeout으로 1분마다 play를 보내봤는데 정상 작동하였음
4. Puppeteer
ㄱ. puppeteer.js
- 이제 main.js에 request body값에 play라는 값이 찍히면 puppeteer을 이용하여 강의를 재생할것
- puppeteer를 main.js에서 불러오고 인자로 유저의 아이디와 패스워드를 넘겨주고 아래와 같이 puppeteer.js를 작성하면
- 아래코드를 간단히 설명하자면
//puppeteer.js
const puppeteer = require("puppeteer");
async function performLoginAndAction(username, password) {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
page.on("dialog", async (dialog) => {
await dialog.accept();
});
await page.goto("url");
console.log("0");
await page.waitForSelector("#input_id");
//로그인
await page.evaluate(
(username, password) => {
const idInput = document.querySelector("#input_id");
const pwInput = document.querySelector("#input_pw");
const loginBtn = document.querySelector("#bt_login");
if (idInput) {
idInput.value = username;
}
if (pwInput) {
pwInput.value = password;
}
if (idInput || pwInput) {
loginBtn.click();
}
},
username,
password
);
await page.waitForNavigation();
await page.goto("url");
await page.waitForSelector(".box");
// 강의 선택
await page.evaluate(() => {
const btn = document.querySelector(".box");
if (btn) {
btn.click();
}
});
await page.waitForSelector(".lec_box button");
// 강의 플레이
await page.evaluate(() => {
const btn4 = document.querySelector(".lec_box");
if (btn4) {
btn4.querySelector("button").click();
}
});
}
module.exports = performLoginAndAction;
5. build
- Electron 앱을 빌드하기 위해
npm install --save-dev electron-builder
하고
- package.json을 수정해줌
{
"name": "siwonscholl_macro",
"version": "1.0.0",
"description": "",
"main": "main.js",
"bin": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron .",
"build": "electron-builder"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.19.2",
"puppeteer": "^22.12.0"
},
"devDependencies": {
"electron": "^31.0.2",
"electron-builder": "^24.3.0"
},
"build": {
"appId": "com.example.siwonscholl_macro",
"files": [
"**/*"
],
"directories": {
"buildResources": "build"
}
}
}
- 터미널에 npm run build를 입력하면 같은 디렉토리에 아래와같이 데스크탑앱이 빌드됨!
6. 화면잠금 모드일때 실행을 하지않는 문제
- 맥을 사용중인데 화면잠금 모드일때는 작동하지않아서 방법을 두가지 생각해봄
- 스크립트만 떼어낸후 화면모드 일때 puppeteer만 작동하게끔 = > 이런게 가능하다고해서 시도
- 컴퓨터를 일정시간에 깨우기 = > 이건 화면잠금모드일때 비밀번호를 입력을 어떻게해야할지..
ㄱ. LaunchAgents
- 터미널에
cd ~/Library/LaunchAgents/
쳐서 해당 경로로 들어간뒤
- 터미널에
nano com.example.electronapp.plist
쳐서 해당파일을 만들고
- 아래의 코드를 넣어둠
ㄴ. puppeteer.js
- 실행할 puppeteer 파일을 원하는 경로에 넣고 수동으로 동작하는지 테스트해보기
ㄷ. 명령어
ㄱ. 아래명령어로 파일의 권한을 설정해줌
chmod 666 "경로/파일명"
plutil ~/Library/LaunchAgents/com.example.electronapp.plist
ㄴ 로그 파일 작성 권한 확인
ㄷ.에이전트 언로드 및 재로드
sudo launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.example.electronapp.plist
sudo launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.electronapp.plist
ㄹ. 로그파일을 확인
log show --predicate 'process == "launchd"' --info --debug | grep com.example.electronapp