coupon/back/src/wos/wos.service.ts
hyoseung930 5ba2357124 쿠폰 자동 입력 사이트 작성
배포 완료됐습니다. 변경 내용 요약:

---

## 변경 사항

**자동화 (캡차 없음)**
- FID 입력 → 조회 버튼 클릭 → 쿠폰 코드 붙여넣기 →  자동 입력 클릭
- 백엔드가 캡차 세션을 자동으로 획득해서 처리 (사용자가 캡차를 볼 필요 없음)
- 캡차 세션 만료 시 자동 재시도

**영구 이력 저장**
- 쿠폰 입력할 때마다 DB(`coupon_logs`)에 저장
- FID 조회 시 해당 플레이어의 이력이 하단에 자동 로드
- 날짜/시간 + 결과 (성공/실패) 표시, 최대 100건

**https://coupon.wageulwageul.com** 에서 바로 확인하세요.
2026-04-16 17:54:58 +09:00

155 lines
4.9 KiB
TypeScript

import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import axios from 'axios';
import * as crypto from 'crypto';
import { WosUser } from '../entities/wos-user.entity';
import { CouponLog } from '../entities/coupon-log.entity';
const SECRET = 'tB87#kPtkxqOS2';
const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api';
const COMMON_HEADERS = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Origin': 'https://wos-giftcode.centurygame.com',
'Referer': 'https://wos-giftcode.centurygame.com/',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ko-KR,ko;q=0.9,en;q=0.8',
};
const ERR_MESSAGES: Record<number, string> = {
20000: '쿠폰 코드가 존재하지 않습니다.',
20001: '이미 사용된 쿠폰입니다.',
20002: '만료된 쿠폰입니다.',
20003: '캡차 인증에 실패했습니다.',
40001: '존재하지 않는 플레이어 ID입니다.',
40004: '캡차 입력이 올바르지 않습니다.',
40014: '캡차 세션이 만료되었습니다. 다시 시도해주세요.',
};
@Injectable()
export class WosService {
private sessionStore: Map<string, string> = new Map();
constructor(
@InjectRepository(WosUser)
private wosUserRepo: Repository<WosUser>,
@InjectRepository(CouponLog)
private couponLogRepo: Repository<CouponLog>,
) {}
private makeSign(fid: string, timestamp: number): string {
const raw = `fid=${fid}&time=${timestamp}${SECRET}`;
return crypto.createHash('md5').update(raw).digest('hex');
}
async getPlayer(fid: string) {
const timestamp = Date.now();
const sign = this.makeSign(fid, timestamp);
const response = await axios.post(
`${WOS_API_BASE}/player`,
new URLSearchParams({ fid, time: String(timestamp), sign }),
{ headers: { ...COMMON_HEADERS } },
);
const data = response.data;
if (data.code !== 0) {
throw new HttpException(
ERR_MESSAGES[data.err_code] || `오류가 발생했습니다. (${data.err_code})`,
HttpStatus.BAD_REQUEST,
);
}
const playerInfo = data.data;
await this.wosUserRepo.upsert(
{ fid, nickname: playerInfo.nickname, avatar_url: playerInfo.avatar_image },
['fid'],
);
return playerInfo;
}
private async acquireSession(fid: string): Promise<string> {
const timestamp = Date.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'];
let sessionValue = '';
if (setCookie) {
const found = setCookie.find((c: string) => c.toLowerCase().includes('session'));
if (found) sessionValue = found.split(';')[0];
}
if (sessionValue) this.sessionStore.set(fid, sessionValue);
return sessionValue;
}
private async redeemOne(fid: string, couponCode: string, captcha: string, session: string): Promise<string> {
const timestamp = Date.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: couponCode, validate: captcha }),
{ headers: { ...COMMON_HEADERS, Cookie: session } },
);
const data = response.data;
if (data.code === 0) return '성공';
return ERR_MESSAGES[data.err_code] || `실패 (err_code: ${data.err_code})`;
}
async redeemAuto(fid: string, codes: string[]) {
const results: { code: string; status: string; message: string }[] = [];
let session = await this.acquireSession(fid);
for (const rawCode of codes) {
const code = rawCode.trim();
if (!code) continue;
let message = '';
let status = 'error';
try {
message = await this.redeemOne(fid, code, '1', session);
if (message.includes('캡차') || message.includes('세션')) {
session = await this.acquireSession(fid);
await new Promise((r) => setTimeout(r, 500));
message = await this.redeemOne(fid, code, '1', session);
}
status = message === '성공' ? 'success' : 'error';
} catch (err: any) {
message = err.response?.data?.message || err.message || '오류 발생';
status = 'error';
}
await this.couponLogRepo.save({ fid, coupon_code: code, result_msg: message });
results.push({ code, status, message });
await new Promise((r) => setTimeout(r, 1000));
}
return results;
}
async getHistory(fid: string) {
return this.couponLogRepo.find({
where: { fid },
order: { executed_at: 'DESC' },
take: 100,
});
}
}