초기 세팅

This commit is contained in:
LHK
2025-10-13 14:32:41 +09:00
부모 6fa0ee185f
커밋 b1c7861ee9
11개의 변경된 파일2857개의 추가작업 그리고 0개의 파일을 삭제

9
.gitignore vendored Normal file
파일 보기

@@ -0,0 +1,9 @@
node_modules/
.env
data/database.sqlite
*.log
npm-debug.log*
.DS_Store
Thumbs.db
.vscode/
.idea/

48
Dockerfile Normal file
파일 보기

@@ -0,0 +1,48 @@
# 1. 베이스 이미지 선택 (Node.js 22 버전)
FROM node:22-slim
# 2. 시스템 패키지 업데이트 및 필수 라이브러리 설치
RUN apt-get update && apt-get install -y \
gconf-service \
libasound2 \
libatk1.0-0 \
libcups2 \
libgdk-pixbuf2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxss1 \
libxtst6 \
ca-certificates \
fonts-liberation \
lsb-release \
xdg-utils \
wget \
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" ]

50
bot.js Normal file
파일 보기

@@ -0,0 +1,50 @@
const db = require('./db');
const AESCipher = require('./crypto');
// const Attendance = require('./attendance'); // 실제 자동화 로직 파일
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}개)`);
const tasks = accountsToProcess.map(account => {
account.DECRYPTED_PSWRD = AESCipher.decrypt(account.DOMAIN_ACCNT_PSWRD);
// TODO: 실제 자동화 로직(Puppeteer)을 실행하는 부분
// 예: return new Attendance(account).run();
console.log(`[작업 실행] 계정: ${account.DOMAIN_ACCNT_ID} / 사이트: ${account.DOMAIN_ADDRS}`);
return Promise.resolve(); // 임시로 즉시 완료
});
await Promise.all(tasks);
console.log("모든 계정 처리가 완료되었습니다.");
} catch (error) {
console.error("봇 실행 중 오류 발생:", error);
}
}
module.exports = { runBot };

29
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
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 };

12
docker-compose.yml Normal file
파일 보기

@@ -0,0 +1,12 @@
services:
bot:
build: .
container_name: attendance-bot
env_file:
- .env
ports:
- "8080:3000" # 내 PC의 8080 포트를 컨테이너의 3000 포트와 연결
volumes:
- ./:/app
- ./data:/app/data
- /app/node_modules

12
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 "$@"

63
index.js Normal file
파일 보기

@@ -0,0 +1,63 @@
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;
// JSON 요청 본문을 파싱하기 위한 미들웨어
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ### 1. 크론 스케줄러 설정 ###
// 5분마다 runBot 함수를 실행
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
`);
// 간단한 HTML 테이블 형태로 결과 표시
let html = `<h1>출석 계정 목록</h1>
<table border="1">
<tr><th>사이트</th><th>계정 ID</th><th>상태</th><th>다음 실행</th><th>사용여부</th></tr>`;
accounts.forEach(acc => {
html += `<tr>
<td>${acc.DOMAIN_NM}</td>
<td>${acc.DOMAIN_ACCNT_ID}</td>
<td>${acc.ATNDNC_STTS_CD}</td>
<td>${acc.ATNDNC_STRT_DTTM}</td>
<td>${acc.USE_YN}</td>
</tr>`;
});
html += '</table>';
res.send(html);
} catch (error) {
res.status(500).send("데이터 조회 중 오류 발생: " + error.message);
}
});
// TODO: 계정 정보 수정, 추가, 삭제 등의 API 엔드포인트를 여기에 추가할 수 있습니다.
// ### 3. 웹 서버 실행 ###
app.listen(PORT, () => {
console.log(`웹 서버가 http://localhost:${PORT} 에서 실행 중입니다. (외부 접속: http://localhost:8080)`);
console.log('자동 출석 봇이 스케줄 대기 중입니다...');
// 서버 시작 시 1회 즉시 실행 (테스트용)
runBot();
});

2463
package-lock.json generated Normal file

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

20
package.json Normal file
파일 보기

@@ -0,0 +1,20 @@
{
"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": {
"dotenv": "^16.4.5",
"express": "^4.19.2",
"fake-useragent": "^1.0.1",
"node-cron": "^3.0.3",
"puppeteer": "^22.12.1",
"sqlite3": "^5.1.7"
}
}

107
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("데이터베이스 초기화 및 연결 종료가 완료되었습니다.");
}
});