coupon/back/src/wos/wos.service.ts
hyoseung930 14956ac9ec 쿠폰 자동 입력 사이트 작성
## 적용된 변경사항

### 백엔드
- **`wos_coupons` 테이블에 `is_expired` 컬럼 추가** (TypeORM synchronize로 자동 생성)
- **`redeemAll` 로직 개선**:
  - 이미 `성공` / `이미 수령` 로그 있는 쿠폰 → 자동 **스킵** (재지급 안 함)
  - `err_code: 40008` (RECEIVED) → **성공** 처리 (`이미 수령` 메시지)
  - `err_code: 40007` (TIME ERROR) → DB에서 `is_expired = true, is_active = false` 업데이트
- **매일 오전 9시 자동 실행** (`@Cron('0 0 9 * * *')`) - 저장된 모든 유저에게 미수령 쿠폰 자동 지급

### 프론트엔드
- **삭제 버튼 제거**
- 만료된 쿠폰은 **보라색 "만료" 배지** 표시, 토글 버튼 숨김
- 활성 쿠폰 카운트에서 만료 제외
2026-04-16 18:23:16 +09:00

219 lines
7.2 KiB
TypeScript

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 axios from 'axios';
import * as crypto from 'crypto';
import { WosUser } from '../entities/wos-user.entity';
import { CouponLog } from '../entities/coupon-log.entity';
import { WosCoupon } from '../entities/wos-coupon.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 SUCCESS_MSG = '성공';
const ALREADY_MSG = '이미 수령';
@Injectable()
export class WosService {
private readonly logger = new Logger(WosService.name);
constructor(
@InjectRepository(WosUser)
private wosUserRepo: Repository<WosUser>,
@InjectRepository(CouponLog)
private couponLogRepo: Repository<CouponLog>,
@InjectRepository(WosCoupon)
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');
}
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(
data.msg || `오류가 발생했습니다. (${data.err_code})`,
HttpStatus.BAD_REQUEST,
);
}
const playerInfo = data.data;
const existing = await this.wosUserRepo.findOne({ where: { fid } });
if (existing) {
existing.nickname = playerInfo.nickname;
existing.avatar_url = playerInfo.avatar_image;
await this.wosUserRepo.save(existing);
} else {
await this.wosUserRepo.save({ fid, nickname: playerInfo.nickname, avatar_url: playerInfo.avatar_image });
}
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];
}
return sessionValue;
}
private async redeemOne(
fid: string,
code: string,
session: string,
): Promise<{ success: boolean; message: string; err_code?: number }> {
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: code, validate: '1' }),
{ headers: { ...COMMON_HEADERS, Cookie: session } },
);
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 };
}
async redeemAll(fid: string) {
const activeCoupons = await this.wosCouponRepo.find({
where: { is_active: true, is_expired: false },
});
if (activeCoupons.length === 0) return [];
const existingLogs = await this.couponLogRepo.find({
where: { fid, result_msg: In([SUCCESS_MSG, ALREADY_MSG]) },
});
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)) {
results.push({ name: coupon.name, code: coupon.code, status: 'skipped', message: '이미 지급 완료' });
continue;
}
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);
}
} catch (err: any) {
result = { success: false, message: err.message || '오류 발생' };
}
if (result.err_code === 40008) {
result = { success: true, message: ALREADY_MSG, err_code: 40008 };
}
if (result.err_code === 40007) {
await this.wosCouponRepo.update(coupon.id, { is_expired: true, is_active: false });
result.message = '만료된 쿠폰';
}
const status = result.success ? 'success' : 'error';
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));
}
return results;
}
@Cron('0 0 9 * * *')
async scheduledRedeemAll() {
this.logger.log('자동 쿠폰 지급 시작...');
const users = await this.wosUserRepo.find();
for (const user of users) {
try {
await this.redeemAll(user.fid);
this.logger.log(`[${user.fid}] ${user.nickname} 지급 완료`);
} catch (err: any) {
this.logger.error(`[${user.fid}] 지급 실패: ${err.message}`);
}
await new Promise((r) => setTimeout(r, 2000));
}
this.logger.log('자동 쿠폰 지급 완료');
}
async getHistory(fid: string) {
return this.couponLogRepo.find({
where: { fid },
order: { executed_at: 'DESC' },
take: 100,
});
}
async listUsers() {
return this.wosUserRepo.find({ order: { created_at: 'DESC' } });
}
async listCoupons() {
return this.wosCouponRepo.find({ order: { created_at: 'DESC' } });
}
async addCoupon(name: string, code: string) {
const exists = await this.wosCouponRepo.findOne({ where: { code } });
if (exists) {
exists.name = name;
exists.is_active = true;
exists.is_expired = false;
return this.wosCouponRepo.save(exists);
}
return this.wosCouponRepo.save({ name, code, is_active: true, is_expired: false });
}
async toggleCoupon(id: number, is_active: boolean) {
await this.wosCouponRepo.update(id, { is_active });
return this.wosCouponRepo.findOne({ where: { id } });
}
async deleteCoupon(id: number) {
await this.wosCouponRepo.delete(id);
return { success: true };
}
}