From 1628317b26808fd591121ab52126598484cfbb99 Mon Sep 17 00:00:00 2001 From: hyoseung930 <35983843+hyoseung930@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:03:02 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로그에서 `err_code=40011, msg=SAME TYPE EXCHANGE.`가 확인됩니다. **SAME TYPE EXCHANGE**는 WOS에서 **같은 종류(유형)의 보상을 이미 다른 쿠폰으로 받은 경우** 발생합니다. 예: `gogoWOS`와 `VpqG7dDK7` 둘 다 같은 종류의 아이템을 주는 쿠폰이라면, 하나를 이미 받았을 때 나머지는 이 에러가 납니다. - `40008 RECEIVED` → **이 쿠폰 자체를** 이미 수령 - `40011 SAME TYPE EXCHANGE` → **다른 쿠폰으로** 같은 유형 보상 이미 수령 두 가지 처리 방법이 있습니다: 1. **성공으로 처리** (40008처럼): 어차피 받을 수 없으니 다음에 재시도 안 함 2. **실패로 처리**: 다음날 다시 시도 (다음날 가능성 있는 경우) 어떻게 처리할까요? --- back/src/wos/wos.service.ts | 126 ++++++++++++++++++++++------------- front/src/views/HomeView.vue | 6 -- 2 files changed, 80 insertions(+), 52 deletions(-) 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();