diff --git a/back/src/wos/wos.service.ts b/back/src/wos/wos.service.ts index 069d7b3..09e82b3 100644 --- a/back/src/wos/wos.service.ts +++ b/back/src/wos/wos.service.ts @@ -1,7 +1,7 @@ import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; -import { Cron, CronExpression } from '@nestjs/schedule'; +import { Cron } from '@nestjs/schedule'; import axios from 'axios'; import * as crypto from 'crypto'; import { WosUser } from '../entities/wos-user.entity'; @@ -10,6 +10,7 @@ import { WosCoupon } from '../entities/wos-coupon.entity'; const SECRET = 'tB87#kPtkxqOS2'; const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api'; +const OCR_SERVER = 'http://127.0.0.1:5001'; const COMMON_HEADERS = { 'Content-Type': 'application/x-www-form-urlencoded', @@ -36,18 +37,49 @@ export class WosService { private wosCouponRepo: Repository, ) {} - private makeSign(fid: string, timestamp: number): string { - const raw = `fid=${fid}&time=${timestamp}${SECRET}`; - return crypto.createHash('md5').update(raw).digest('hex'); - } - private now(): number { return Date.now(); } + private makePlayerSign(fid: string, timestamp: number): string { + const raw = `fid=${fid}&time=${timestamp}${SECRET}`; + return crypto.createHash('md5').update(raw).digest('hex'); + } + + private makeCaptchaSign(fid: string, timestamp: number): string { + const raw = `fid=${fid}&init=0&time=${timestamp}${SECRET}`; + return crypto.createHash('md5').update(raw).digest('hex'); + } + + private makeGiftSign(fid: string, timestamp: number, cdk: string, captchaCode: string): string { + const raw = `captcha_code=${captchaCode}&cdk=${cdk}&fid=${fid}&time=${timestamp}${SECRET}`; + return crypto.createHash('md5').update(raw).digest('hex'); + } + + private async solveCaptcha(fid: string): Promise { + const ts = this.now(); + const sign = this.makeCaptchaSign(fid, ts); + const resp = await axios.post( + `${WOS_API_BASE}/captcha`, + new URLSearchParams({ fid, time: String(ts), sign, init: '0' }), + { headers: { ...COMMON_HEADERS } }, + ); + if (resp.data.code !== 0) throw new Error(`Captcha 요청 실패: ${resp.data.msg}`); + + const imgB64: string = resp.data.data.img; + const imgData = imgB64.includes(',') ? imgB64.split(',')[1] : imgB64; + const imgBuffer = Buffer.from(imgData, 'base64'); + + const ocrResp = await axios.post(OCR_SERVER, imgBuffer, { + headers: { 'Content-Type': 'application/octet-stream' }, + timeout: 5000, + }); + return String(ocrResp.data).trim().toLowerCase(); + } + async getPlayer(fid: string) { const timestamp = this.now(); - const sign = this.makeSign(fid, timestamp); + const sign = this.makePlayerSign(fid, timestamp); const response = await axios.post( `${WOS_API_BASE}/player`, @@ -77,40 +109,50 @@ export class WosService { return playerInfo; } - private async acquireSession(fid: string): Promise { - const timestamp = this.now(); - const sign = this.makeSign(fid, timestamp); - - const response = await axios.post( - `${WOS_API_BASE}/captcha`, - new URLSearchParams({ fid, time: String(timestamp), sign }), - { headers: { ...COMMON_HEADERS }, withCredentials: true }, - ); - - const setCookie = response.headers['set-cookie']; - if (setCookie && setCookie.length > 0) { - return setCookie.map((c: string) => c.split(';')[0]).join('; '); - } - return ''; - } - private async redeemOne( fid: string, code: string, - session: string, + maxRetries = 5, ): Promise<{ success: boolean; message: string; err_code?: number }> { - const timestamp = this.now(); - const sign = this.makeSign(fid, timestamp); - - const response = await axios.post( - `${WOS_API_BASE}/gift_code`, - new URLSearchParams({ fid, time: String(timestamp), sign, cdk: code, validate: '1' }), - { headers: { ...COMMON_HEADERS, Cookie: session } }, + const ts1 = this.now(); + const playerSign = this.makePlayerSign(fid, ts1); + await axios.post( + `${WOS_API_BASE}/player`, + new URLSearchParams({ fid, time: String(ts1), sign: playerSign }), + { headers: { ...COMMON_HEADERS } }, ); - const data = response.data; - if (data.code === 0) return { success: true, message: SUCCESS_MSG }; - return { success: false, message: data.msg || `실패 (${data.err_code})`, err_code: data.err_code }; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + let captchaCode: string; + try { + captchaCode = await this.solveCaptcha(fid); + } catch (err: any) { + return { success: false, message: `캡차 오류: ${err.message}` }; + } + + const ts2 = this.now(); + const giftSign = this.makeGiftSign(fid, ts2, code, captchaCode); + const response = await axios.post( + `${WOS_API_BASE}/gift_code`, + new URLSearchParams({ fid, time: String(ts2), sign: giftSign, cdk: code, captcha_code: captchaCode }), + { headers: { ...COMMON_HEADERS } }, + ); + + const data = response.data; + this.logger.log(`[${fid}] ${code} attempt=${attempt} captcha=${captchaCode} → code=${data.code} err=${data.err_code} msg=${data.msg}`); + + if (data.code === 0) return { success: true, message: SUCCESS_MSG }; + if (data.err_code === 40103) { + if (attempt < maxRetries) { + await new Promise((r) => setTimeout(r, 500)); + continue; + } + return { success: false, message: `캡차 인식 실패 (${maxRetries}회 시도)`, err_code: data.err_code }; + } + return { success: false, message: data.msg || `실패 (${data.err_code})`, err_code: data.err_code }; + } + + return { success: false, message: '알 수 없는 오류' }; } async redeemAll(fid: string) { @@ -125,7 +167,6 @@ export class WosService { const alreadySucceeded = new Set(existingLogs.map((l) => l.coupon_code)); const results: { name: string; code: string; status: string; message: string }[] = []; - let session = await this.acquireSession(fid); for (const coupon of activeCoupons) { if (alreadySucceeded.has(coupon.code)) { @@ -135,13 +176,7 @@ export class WosService { let result: { success: boolean; message: string; err_code?: number }; try { - result = await this.redeemOne(fid, coupon.code, session); - - if (result.err_code === 40014 || result.err_code === 40004) { - session = await this.acquireSession(fid); - await new Promise((r) => setTimeout(r, 500)); - result = await this.redeemOne(fid, coupon.code, session); - } + result = await this.redeemOne(fid, coupon.code); } catch (err: any) { result = { success: false, message: err.message || '오류 발생' }; } @@ -159,7 +194,7 @@ export class WosService { await this.couponLogRepo.save({ fid, coupon_code: coupon.code, result_msg: result.message }); results.push({ name: coupon.name, code: coupon.code, status, message: result.message }); - await new Promise((r) => setTimeout(r, 1000)); + await new Promise((r) => setTimeout(r, 1500)); } return results; @@ -224,8 +259,7 @@ export class WosService { if (alreadyDone) continue; try { - const session = await this.acquireSession(user.fid); - const result = await this.redeemOne(user.fid, code, session); + const result = await this.redeemOne(user.fid, code); const finalResult = result.err_code === 40008 diff --git a/front/src/views/HomeView.vue b/front/src/views/HomeView.vue index 74a80da..efbd04a 100644 --- a/front/src/views/HomeView.vue +++ b/front/src/views/HomeView.vue @@ -46,11 +46,6 @@ -
- -

아직 입력 이력이 없습니다.

-
-
@@ -62,7 +57,6 @@ import { ref, onMounted } from 'vue'; import { useWosStore } from '../stores/wos.store'; import UserCard from '../components/UserCard.vue'; import CouponInput from '../components/CouponInput.vue'; -import HistoryList from '../components/HistoryList.vue'; import CouponManager from '../components/CouponManager.vue'; const store = useWosStore();