브라우징 자동화에만 집중
This commit is contained in:
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/
|
||||
71
backup/Dockerfile
Normal file
71
backup/Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
# 1. 베이스 이미지 선택 (Node.js 22 버전)
|
||||
FROM node:22-slim
|
||||
|
||||
# 2. Puppeteer 실행에 필요한 모든 시스템 의존성 라이브러리 설치 (완벽 버전)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
# Puppeteer Core Dependencies
|
||||
ca-certificates \
|
||||
fonts-liberation \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libatk1.0-0 \
|
||||
libcairo2 \
|
||||
libcups2 \
|
||||
libdbus-1-3 \
|
||||
libexpat1 \
|
||||
libfontconfig1 \
|
||||
libgbm1 \
|
||||
libgcc1 \
|
||||
libgconf-2-4 \
|
||||
libgdk-pixbuf2.0-0 \
|
||||
libglib2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libnspr4 \
|
||||
libnss3 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libstdc++6 \
|
||||
libx11-6 \
|
||||
libx11-xcb1 \
|
||||
libxcb1 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxdamage1 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
lsb-release \
|
||||
wget \
|
||||
xdg-utils \
|
||||
# Tesseract OCR
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
--no-install-recommends --quiet && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 3. 작업 폴더 생성 및 지정
|
||||
WORKDIR /app
|
||||
|
||||
# 4. package.json과 package-lock.json을 먼저 복사
|
||||
COPY package*.json ./
|
||||
|
||||
# 5. 의존성 설치
|
||||
RUN npm install
|
||||
|
||||
# 6. 소스 코드 복사
|
||||
COPY . .
|
||||
|
||||
# 7. entrypoint 스크립트를 복사하고 실행 권한 부여
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# 8. 컨테이너의 진입점으로 entrypoint 스크립트 지정
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
|
||||
# 9. entrypoint에 전달될 기본 명령어 (메인 애플리케이션)
|
||||
CMD [ "node", "index.js" ]
|
||||
0
backup/README.md
Normal file
0
backup/README.md
Normal file
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;
|
||||
58
backup/bot.js
Normal file
58
backup/bot.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const db = require('./db');
|
||||
const AESCipher = require('./crypto');
|
||||
const Attendance = require('./attendance');
|
||||
const ua = require('fake-useragent');
|
||||
|
||||
async function runBot() {
|
||||
console.log(`[${new Date().toISOString()}] 자동 출석 체크를 시작합니다...`);
|
||||
|
||||
try {
|
||||
const limit = process.env.GET_DOMAIN_COUNT || 5;
|
||||
|
||||
const accountsToProcess = 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]);
|
||||
|
||||
if (accountsToProcess.length === 0) {
|
||||
console.log("처리할 계정이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`총 ${accountsToProcess.length}개의 계정을 처리합니다. (최대 ${limit}개)`);
|
||||
|
||||
for (const account of accountsToProcess) {
|
||||
// 상태를 '진행중(2)'으로 먼저 업데이트하여 다른 프로세스가 중복 실행하지 않도록 잠금
|
||||
await db.run(
|
||||
"UPDATE DOMAIN_ACCNT_LIST SET ATNDNC_STTS_CD = '2' WHERE DOMAIN_SEQ_ID = ? AND DOMAIN_ACCNT_ID = ?",
|
||||
[account.DOMAIN_SEQ_ID, account.DOMAIN_ACCNT_ID]
|
||||
);
|
||||
|
||||
account.DOMAIN_ACCNT_PSWRD = AESCipher.decrypt(account.DOMAIN_ACCNT_PSWRD);
|
||||
|
||||
// DB에 저장된 값이 없으면, 여기서 새로운 User-Agent를 생성하여 account 객체에 주입
|
||||
if (!account.USER_AGENT) {
|
||||
account.USER_AGENT = ua();
|
||||
}
|
||||
|
||||
// 모든 준비가 끝난 account 데이터를 전달하여 Attendance 작업 실행
|
||||
const attendanceTask = new Attendance(account);
|
||||
await attendanceTask.run();
|
||||
}
|
||||
|
||||
console.log("모든 계정 처리가 완료되었습니다.");
|
||||
|
||||
} catch (error) {
|
||||
console.error("봇 실행 중 오류 발생:", error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runBot };
|
||||
29
backup/crypto.js
Normal file
29
backup/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
backup/db.js
Normal file
44
backup/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 };
|
||||
14
backup/docker-compose.yml
Normal file
14
backup/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: attendance-bot
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8080:3000" # 내 PC의 8080 포트를 컨테이너의 3000 포트와 연결
|
||||
volumes:
|
||||
- ./:/app
|
||||
- ./data:/app/data
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- TZ=Asia/Seoul
|
||||
12
backup/docker-entrypoint.sh
Normal file
12
backup/docker-entrypoint.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 혹시라도 스크립트 실행 중 오류가 발생하면 즉시 중단
|
||||
set -e
|
||||
|
||||
# 1. 데이터베이스 초기화 스크립트 실행
|
||||
echo "Entrypoint: 데이터베이스 상태를 확인하고 필요시 초기화합니다..."
|
||||
node setupDatabase.js
|
||||
|
||||
# 2. 초기화 작업이 끝나면, Dockerfile의 CMD로 전달된 메인 명령어를 실행
|
||||
echo "Entrypoint: 초기화 완료. 메인 애플리케이션을 시작합니다..."
|
||||
exec "$@"
|
||||
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("데이터베이스 초기화 및 연결 종료가 완료되었습니다.");
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user