commit e07c7714386c38cd8a44eb0f98aec62152d5f819 Author: hojin.jeong Date: Mon Aug 25 16:10:19 2025 +0900 version 0.0.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4e554d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +.run +node_modules +test +yarn.lock \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..aac013e --- /dev/null +++ b/app.js @@ -0,0 +1,52 @@ + +const Fs = require('fs') + +const Lotto = require('./lib/lotto') +const NTFY = require('./lib/ntfy') +const Constants = require('./lib/constants') + +new class LottoAutomation { + #config = {} + + #_start() { + this.#_loadConfig() + setImmediate(this.#_buyLotto.bind(this)) + } + #_loadConfig() { + if(!Fs.existsSync(Constants.CONFIG_PATH)) + throw new Error('Config.json 파일이 존재하지 않습니다.') + + try { + const configString = Fs.readFileSync(Constants.CONFIG_PATH) + this.#config = JSON.parse(configString) + NTFY.Init(this.#config.ntfyUrl, this.#config.ntfyToken) + } catch(e) { + throw new Error('Config.json 파일이 올바르지 않습니다.') + } + } + async #_buyLotto() { + const loginResult = await Lotto.Login(this.#config.id, this.#config.password) + if(loginResult instanceof Error) + return NTFY.Send('로그인 실패 - ' + loginResult.message ?? loginResult.description, 'no_entry_sign') + + const buyResult = await Lotto.Buy(this.#config.buyLottoPolicy) + if(buyResult instanceof Error) + return NTFY.Send('구입 실패 - ' + loginResult.message ?? loginResult.description, 'no_entry_sign') + + const successMessageArr = [ + `구매 성공`, + `ROUND: ${buyResult.round}`, + `COST : ${buyResult.cost}`, + ] + successMessageArr.push( + ...buyResult.numbers.map(numberStr => { + const numberType = Constants.LOTTO_BUY_TYPES[numberStr.slice(-1)] + const [idx, ...numbers] = numberStr.slice(0, -1).split('|') + return `NUMBER ${idx} [${numberType}]: ${numbers.join(', ')}` + }) + ) + NTFY.Send(successMessageArr.join('\n'), 'tada') + } + + constructor() { setImmediate(this.#_start.bind(this)) } +} \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..491c54d --- /dev/null +++ b/config.json @@ -0,0 +1,10 @@ +{ + "id": "USERID", + "password": "PASSWORD", + "ntfyUrl": "URL", + "ntfyToken": "TOKEN", + "buyLottoPolicy": [ + "auto", + "1,2,3,4,5,6" + ] +} \ No newline at end of file diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..9ebbcd0 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,44 @@ + +class Constants { + static BASE_URL = 'https://dhlottery.co.kr' + static API_URL = 'https://ol.dhlottery.co.kr' + static DEFAULT_SESSION_URL = `${Constants.BASE_URL}/gameResult.do?method=byWin&wiselog=H_C_1_1` + static SYSTEM_CHECK_URL = `${Constants.BASE_URL}/index_check.html` + static MAIN_PAGE_URL = `${Constants.BASE_URL}/common.do?method=main` + static LOGIN_REQUEST_URL = `${Constants.BASE_URL}/userSsl.do?method=login` + static BUY_PAGE_URL = `${Constants.API_URL}/olotto/game/game645.do` + static BUY_LOTTO_645_URL = `${Constants.API_URL}/olotto/game/execBuy.do` + static BUY_READY_SOCKET_URL = `${Constants.API_URL}/olotto/game/egovUserReadySocket.json` + static ROUND_INFO_URL = `${Constants.BASE_URL}/common.do?method=main` + + static DEFAULT_TIMEOUT = 10 * 1000 + + static DEFAULT_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36', + 'Connection': 'keep-alive', + 'Cache-Control': 'max-age=0', + 'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"', + 'sec-ch-ua-mobile': '?0"', + 'Upgrade-Insecure-Requests': '1', + 'Origin': Constants.BASE_URL, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Referer': Constants.BASE_URL, + 'Sec-Fetch-Site': 'same-site', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8,ko-KR;q=0.7', + } + + static LOTTO_BUY_TYPES = { + '1': '수동', + '2': '반자동', + '3': '자동' + } + + static CONFIG_FILENAME = process.env.LOTTO_AUTOMATION_CONFIG_FILENAME ?? 'Config.json' + static CONFIG_PATH = `${process.env.LOTTO_AUTOMATION_CONFIG_DIR ?? '.'}/${Constants.CONFIG_FILENAME}` +} + +module.exports = Constants \ No newline at end of file diff --git a/lib/lotto.js b/lib/lotto.js new file mode 100644 index 0000000..5cd2739 --- /dev/null +++ b/lib/lotto.js @@ -0,0 +1,122 @@ + +const Axios = require('axios') +const Cheerio = require('cheerio') + +const Constants = require('./constants') + +class Lotto { + static #SessionID = undefined + static #LottoDirect = undefined + + static #_request(method, url, data) { + const requestOpts = { + method, + url, + data, + headers: Constants.DEFAULT_HEADERS, + timeout: Constants.DEFAULT_TIMEOUT, + } + if(this.#SessionID) + requestOpts.headers.Cookie = this.#SessionID + return Axios(requestOpts).catch(err => err) + } + static async #_syncSession() { + const sessionResponse = await this.#_request('GET', Constants.DEFAULT_SESSION_URL) + if(sessionResponse instanceof Error) + return sessionResponse + + if(sessionResponse.request.res.responseUrl === Constants.SYSTEM_CHECK_URL) + return new Error('동행복권 사이트가 현재 시스템 점검중입니다.') + + const sessionId = sessionResponse.headers['set-cookie'] + .find(cookieStr => cookieStr.indexOf('JSESSIONID') !== -1) + if(!sessionId) + return new Error('쿠키가 정상적으로 세팅되지 않았습니다.') + + this.#SessionID = sessionId + } + static async #_getRound() { + const roundResponse = await this.#_request('GET', Constants.ROUND_INFO_URL) + if(roundResponse instanceof Error) + return roundResponse + + const $ = Cheerio.load(roundResponse.data) + const lastDrawnRound = parseInt($("strong#lottoDrwNo").text(), 10) + const round = lastDrawnRound + 1 + return round + } + + static async Login(userId, userPw) { + const sessionSyncError = await this.#_syncSession() + if(sessionSyncError instanceof Error) + return sessionSyncError + + const loginData = { + returnUrl: Constants.MAIN_PAGE_URL, + userId, + password: userPw, + checkSave: 'off', + newsEventYn: '', + } + const loginResult = await this.#_request('POST', Constants.LOGIN_REQUEST_URL, loginData) + if(loginResult instanceof Error) + return loginResult + + const $ = Cheerio.load(loginResult.data) + const btnCommonElements = $('a.btn_common') + if(btnCommonElements.length > 0) + return new Error('로그인에 실패했습니다. 아이디 또는 비밀번호를 확인해주세요.') + + return true + } + static async Buy(lotteryInputs = []) { + if(lotteryInputs.length === 0) + return new Error('구매할 로또 정보를 입력해주세요.') + if(lotteryInputs.length > 5) + lotteryInputs = lotteryInputs.slice(0, 5) + + const round = await this.#_getRound() + + const readySocketResponse = await this.#_request('POST', Constants.BUY_READY_SOCKET_URL) + if(readySocketResponse instanceof Error) + return readySocketResponse + + this.#LottoDirect = readySocketResponse.data.ready_ip + + const requestParams = lotteryInputs.map((lotto, idx) => { + return { + genType: lotto === 'auto' ? '0' : '1', + arrGameChoiceNum: lotto === 'auto' ? null : lotto.map(num => String(num).padStart(2, 0)).join(','), + alpabet: String.fromCharCode(65 + idx) + } + }) + const requestData = { + round: String(round), + direct: this.#LottoDirect, + nBuyAmount: `${requestParams.length * 1000}`, + param: JSON.stringify(requestParams), + gameCnt: `${requestParams.length}`, + } + const buyResponse = await this.#_request('POST', Constants.BUY_LOTTO_645_URL, requestData) + if(buyResponse instanceof Error) + return buyResponse + + const { result } = buyResponse.data + + if ((result.resultMsg || "FAILURE").toUpperCase() !== "SUCCESS") + return new Error(result.resultMsg) + + return { + round: result.buyRound, + barcode: Object.keys(result) + .filter(k => k.startsWith('barCode')) + .map(k => result[k]) + .join(' '), + cost: result.nBuyAmount, + numbers: result.arrGameChoiceNum, + message: result.resultMsg, + } + } +} + +module.exports = Lotto \ No newline at end of file diff --git a/lib/ntfy.js b/lib/ntfy.js new file mode 100644 index 0000000..9bfffa7 --- /dev/null +++ b/lib/ntfy.js @@ -0,0 +1,29 @@ + +const Axios = require('axios') + +class NTFY { + static #url + static #token + + static Init(url, token) { + this.#url = url + this.#token = token + } + static Send(message, tags) { + if(!this.#url || !this.#token) + return + + const requestOpts = { + method: 'POST', + url: this.#url, + headers: { + Authorization: `Bearer ${this.#token}`, + Tags: tags, + }, + data: message + } + return Axios(requestOpts).catch(err => err) + } +} + +module.exports = NTFY \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..291aca1 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "lotto_automation", + "version": "0.0.1", + "description": "Lottery Automation Service", + "main": "app.js", + "directories": { + "lib": "lib", + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "sunha.park (sunha321@gmail.com)", + "contributors": [ + "sunha.park (sunha321@gmail.com)", + "hojin.jeong (a66764765@gmail.com)" + ], + "license": "MIT", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", + "dependencies": { + "axios": "^1.11.0", + "cheerio": "^1.1.2" + } +}