쿠폰 자동 입력 사이트 작성
완성됐습니다. 전체 구조:
---
## 📁 autoCoupon 프로젝트 구조
### `autoCoupon/back/` (NestJS)
- **[./autoCoupon/back/src/main.ts](./autoCoupon/back/src/main.ts)** — 진입점, CORS 설정, 포트 3000
- **[./autoCoupon/back/src/app.module.ts](./autoCoupon/back/src/app.module.ts)** — TypeORM MySQL 연결 (`kakao` DB)
- **[./autoCoupon/back/src/wos/wos.service.ts](./autoCoupon/back/src/wos/wos.service.ts)** — MD5 sign 생성, 세션(쿠키) 메모리 저장, 쿠폰 순차 입력(1초 딜레이)
- **[./autoCoupon/back/src/wos/wos.controller.ts](./autoCoupon/back/src/wos/wos.controller.ts)** — `POST /api/player`, `/api/captcha`, `/api/redeem`
- **[./autoCoupon/back/src/entities/](./autoCoupon/back/src/entities/)** — `wos_users`, `coupon_logs` 테이블 (TypeORM synchronize: true로 자동 생성)
### `autoCoupon/front/` (Vue 3 + Pinia)
- **[./autoCoupon/front/src/stores/wos.store.ts](./autoCoupon/front/src/stores/wos.store.ts)** — Pinia store (플레이어 조회, 캡차, 쿠폰 제출)
- **[./autoCoupon/front/src/components/UserCard.vue](./autoCoupon/front/src/components/UserCard.vue)** — 닉네임, 용광로 레벨, 아바타 카드
- **[./autoCoupon/front/src/components/CouponInput.vue](./autoCoupon/front/src/components/CouponInput.vue)** — 캡차 + 쿠폰 일괄 입력 (줄바꿈 구분)
- **[./autoCoupon/front/src/components/ResultTable.vue](./autoCoupon/front/src/components/ResultTable.vue)** — 쿠폰별 성공/실패 결과 테이블
---
## 시작 방법
**백엔드:**
```bash
cd autoCoupon/back
cp .env.example .env
npm install
npm run start:dev
```
**프론트엔드:**
```bash
cd autoCoupon/front
npm install
npm run dev
```
> **배포 시** Nginx에서 `/` → Vue `dist/`, `/api` → `localhost:3000` 으로 프록시 설정하면 됩니다.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { WosService } from './wos.service';
|
||||
|
||||
@Controller('api')
|
||||
export class WosController {
|
||||
constructor(private readonly wosService: WosService) {}
|
||||
|
||||
@Post('player')
|
||||
@HttpCode(200)
|
||||
async getPlayer(@Body() body: { fid: string }) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WosController } from './wos.controller';
|
||||
import { WosService } from './wos.service';
|
||||
import { WosUser } from '../entities/wos-user.entity';
|
||||
import { CouponLog } from '../entities/coupon-log.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([WosUser, CouponLog])],
|
||||
controllers: [WosController],
|
||||
providers: [WosService],
|
||||
})
|
||||
export class WosModule {}
|
||||
@@ -0,0 +1,162 @@
|
||||
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 ERR_MESSAGES: Record<number, string> = {
|
||||
20000: '쿠폰 코드가 존재하지 않습니다.',
|
||||
20001: '이미 사용된 쿠폰입니다.',
|
||||
20002: '만료된 쿠폰입니다.',
|
||||
20003: '캡차 인증에 실패했습니다.',
|
||||
40014: '캡차 세션이 만료되었습니다. 다시 시도해주세요.',
|
||||
40004: '캡차 입력이 올바르지 않습니다.',
|
||||
};
|
||||
|
||||
@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: { 'Content-Type': 'application/x-www-form-urlencoded' } },
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async getCaptcha(fid: 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: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
withCredentials: true,
|
||||
},
|
||||
);
|
||||
|
||||
const setCookie = response.headers['set-cookie'];
|
||||
if (setCookie) {
|
||||
const sessionCookie = setCookie.find((c: string) => c.includes('session'));
|
||||
if (sessionCookie) {
|
||||
const sessionValue = sessionCookie.split(';')[0];
|
||||
this.sessionStore.set(fid, sessionValue);
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
async redeemCoupon(fid: string, couponCode: string, captcha: 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: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Cookie: sessionCookie,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
async redeemMultiple(fid: string, codes: string[], captcha: string) {
|
||||
const results: { code: string; status: string; message: string }[] = [];
|
||||
|
||||
for (const code of codes) {
|
||||
try {
|
||||
await this.redeemCoupon(fid, code.trim(), captcha);
|
||||
results.push({ code, status: 'success', message: '성공' });
|
||||
} catch (err: any) {
|
||||
results.push({
|
||||
code,
|
||||
status: 'error',
|
||||
message: err.message || '실패',
|
||||
});
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user