쿠폰 자동 입력 사이트 작성

로그에서 `err_code=40011, msg=SAME TYPE EXCHANGE.`가 확인됩니다.

**SAME TYPE EXCHANGE**는 WOS에서 **같은 종류(유형)의 보상을 이미 다른 쿠폰으로 받은 경우** 발생합니다.

예: `gogoWOS`와 `VpqG7dDK7` 둘 다 같은 종류의 아이템을 주는 쿠폰이라면, 하나를 이미 받았을 때 나머지는 이 에러가 납니다.

- `40008 RECEIVED` → **이 쿠폰 자체를** 이미 수령
- `40011 SAME TYPE EXCHANGE` → **다른 쿠폰으로** 같은 유형 보상 이미 수령

두 가지 처리 방법이 있습니다:

1. **성공으로 처리** (40008처럼): 어차피 받을 수 없으니 다음에 재시도 안 함
2. **실패로 처리**: 다음날 다시 시도 (다음날 가능성 있는 경우)

어떻게 처리할까요?
This commit is contained in:
hyoseung930 2026-04-17 10:03:02 +09:00
parent 107f1ffe84
commit 1628317b26
2 changed files with 80 additions and 52 deletions

View File

@ -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<WosCoupon>,
) {}
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<string> {
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,42 +109,52 @@ export class WosService {
return playerInfo;
}
private async acquireSession(fid: string): Promise<string> {
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 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 } },
);
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(timestamp), sign, cdk: code, validate: '1' }),
{ headers: { ...COMMON_HEADERS, Cookie: session } },
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) {
const activeCoupons = await this.wosCouponRepo.find({
where: { is_active: true, is_expired: false },
@ -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

View File

@ -46,11 +46,6 @@
<CouponInput />
</div>
<div class="card" v-if="store.player">
<HistoryList />
<p class="empty-history" v-if="store.history.length === 0">아직 입력 이력이 없습니다.</p>
</div>
<div class="card admin-card">
<CouponManager />
</div>
@ -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();