쿠폰 자동 입력 사이트 작성

배포 완료됐습니다. 변경 내용 요약:

---

## 변경 사항

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

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

**https://coupon.wageulwageul.com** 에서 바로 확인하세요.
This commit is contained in:
hyoseung930
2026-04-16 17:54:58 +09:00
parent 3461886b48
commit 5ba2357124
6 changed files with 258 additions and 214 deletions
+8 -9
View File
@@ -1,4 +1,4 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { Controller, Post, Get, Body, Param, HttpCode } from '@nestjs/common';
import { WosService } from './wos.service';
@Controller('api')
@@ -11,15 +11,14 @@ export class WosController {
return this.wosService.getPlayer(body.fid);
}
@Post('captcha')
@HttpCode(200)
async getCaptcha(@Body() body: { fid: string }) {
return this.wosService.getCaptcha(body.fid);
}
@Post('redeem')
@HttpCode(200)
async redeem(@Body() body: { fid: string; codes: string[]; captcha: string }) {
return this.wosService.redeemMultiple(body.fid, body.codes, body.captcha);
async redeem(@Body() body: { fid: string; codes: string[] }) {
return this.wosService.redeemAuto(body.fid, body.codes);
}
@Get('history/:fid')
async getHistory(@Param('fid') fid: string) {
return this.wosService.getHistory(fid);
}
}
+49 -67
View File
@@ -12,10 +12,10 @@ 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',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'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> = {
@@ -65,108 +65,90 @@ export class WosService {
const playerInfo = data.data;
await this.wosUserRepo.upsert(
{
fid,
nickname: playerInfo.nickname,
avatar_url: playerInfo.avatar_image,
},
{ fid, nickname: playerInfo.nickname, avatar_url: playerInfo.avatar_image },
['fid'],
);
return playerInfo;
}
async getCaptcha(fid: string) {
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,
},
{ headers: { ...COMMON_HEADERS }, withCredentials: true },
);
const setCookie = response.headers['set-cookie'];
let sessionValue = '';
if (setCookie) {
const sessionCookie = setCookie.find((c: string) => c.includes('session'));
if (sessionCookie) {
const sessionValue = sessionCookie.split(';')[0];
this.sessionStore.set(fid, sessionValue);
}
const found = setCookie.find((c: string) => c.toLowerCase().includes('session'));
if (found) sessionValue = found.split(';')[0];
}
const data = response.data;
if (data.code !== 0) {
throw new HttpException(
ERR_MESSAGES[data.err_code] || `캡차 로드 실패 (${data.err_code})`,
HttpStatus.BAD_REQUEST,
);
}
return { img: data.data?.img, captcha_id: data.data?.captcha_id };
if (sessionValue) this.sessionStore.set(fid, sessionValue);
return sessionValue;
}
async redeemCoupon(fid: string, couponCode: string, captcha: string) {
private async redeemOne(fid: string, couponCode: string, captcha: string, session: string): Promise<string> {
const timestamp = Date.now();
const sign = this.makeSign(fid, timestamp);
const sessionCookie = this.sessionStore.get(fid) || '';
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: sessionCookie,
},
},
new URLSearchParams({ fid, time: String(timestamp), sign, cdk: couponCode, validate: captcha }),
{ headers: { ...COMMON_HEADERS, Cookie: session } },
);
const data = response.data;
const resultMsg =
data.code === 0
? '성공'
: ERR_MESSAGES[data.err_code] || `실패 (err_code: ${data.err_code})`;
await this.couponLogRepo.save({
fid,
coupon_code: couponCode,
result_msg: resultMsg,
});
if (data.code !== 0) {
throw new HttpException(resultMsg, HttpStatus.BAD_REQUEST);
}
return { message: resultMsg };
if (data.code === 0) return '성공';
return ERR_MESSAGES[data.err_code] || `실패 (err_code: ${data.err_code})`;
}
async redeemMultiple(fid: string, codes: string[], captcha: string) {
async redeemAuto(fid: string, codes: string[]) {
const results: { code: string; status: string; message: string }[] = [];
for (const code of codes) {
let session = await this.acquireSession(fid);
for (const rawCode of codes) {
const code = rawCode.trim();
if (!code) continue;
let message = '';
let status = 'error';
try {
await this.redeemCoupon(fid, code.trim(), captcha);
results.push({ code, status: 'success', message: '성공' });
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) {
results.push({
code,
status: 'error',
message: err.message || '실패',
});
message = err.response?.data?.message || err.message || '오류 발생';
status = 'error';
}
await new Promise((resolve) => setTimeout(resolve, 1000));
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,
});
}
}