initial commit

This commit is contained in:
2026-05-11 16:35:26 +09:00
commit 6372c699e6
35 changed files with 8602 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
import { Controller, Post, HttpCode } from '@nestjs/common';
import { CrawlerService } from './crawler.service';
@Controller('api/crawler')
export class CrawlerController {
constructor(private readonly crawlerService: CrawlerService) {}
@Post('trigger')
@HttpCode(200)
async triggerCrawl() {
return this.crawlerService.crawlWiki();
}
}
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CrawlerController } from './crawler.controller';
import { CrawlerService } from './crawler.service';
import { WosCoupon } from '../entities/wos-coupon.entity';
import { WosModule } from '../wos/wos.module';
@Module({
imports: [TypeOrmModule.forFeature([WosCoupon]), WosModule],
controllers: [CrawlerController],
providers: [CrawlerService],
})
export class CrawlerModule {}
+113
View File
@@ -0,0 +1,113 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cron } from '@nestjs/schedule';
import axios from 'axios';
import { WosCoupon } from '../entities/wos-coupon.entity';
import { WosService } from '../wos/wos.service';
const WIKI_URL = 'https://www.whiteoutsurvival.wiki/giftcodes/';
@Injectable()
export class CrawlerService {
private readonly logger = new Logger(CrawlerService.name);
constructor(
@InjectRepository(WosCoupon)
private wosCouponRepo: Repository<WosCoupon>,
private readonly wosService: WosService,
) {}
private extractCodesFromHtml(html: string): string[] {
const found: string[] = [];
const pattern = /<span[^>]*class="code"[^>]*>([^<]+)<\/span>/g;
let match: RegExpExecArray | null;
while ((match = pattern.exec(html)) !== null) {
const code = match[1].trim();
if (code.length > 0) found.push(code);
}
return found;
}
async crawlWiki(): Promise<{ found: number; added: string[] }> {
this.logger.log('Wiki 쿠폰 크롤링 시작...');
let html: string;
try {
const resp = await axios.get(WIKI_URL, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html',
},
timeout: 10000,
});
html = resp.data as string;
} catch (err: any) {
this.logger.error(`Wiki 요청 실패: ${err.message}`);
return { found: 0, added: [] };
}
const wikiCodes = this.extractCodesFromHtml(html);
this.logger.log(`Wiki에서 ${wikiCodes.length}개 코드 발견`);
if (wikiCodes.length === 0) return { found: 0, added: [] };
const existingCoupons = await this.wosCouponRepo.find();
const existingCodes = new Set(existingCoupons.map((c) => c.code));
const addedCodes: string[] = [];
for (const code of wikiCodes) {
if (existingCodes.has(code)) continue;
this.logger.log(`새 쿠폰 발견: ${code}`);
try {
await this.wosService.addCoupon(`Wiki: ${code}`, code);
existingCodes.add(code);
addedCodes.push(code);
this.logger.log(`쿠폰 추가 완료: ${code}`);
} catch (err: any) {
this.logger.error(`쿠폰 추가 실패 [${code}]: ${err.message}`);
}
}
if (addedCodes.length > 0) {
await this.sendDiscordNotification(addedCodes);
}
this.logger.log(`Wiki 크롤링 완료 - 신규 쿠폰: ${addedCodes.length}`);
return { found: wikiCodes.length, added: addedCodes };
}
private async sendDiscordNotification(codes: string[]): Promise<void> {
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
if (!webhookUrl) return;
const codeList = codes.map((c) => `\`${c}\``).join(', ');
const payload = {
username: 'WOS 쿠폰 봇',
avatar_url: 'https://wos-giftcode.centurygame.com/favicon.ico',
embeds: [
{
title: '🎁 새 WOS 쿠폰 발견!',
description: `Wiki에서 새로운 쿠폰 **${codes.length}개**를 찾았어요!\n\n${codeList}`,
color: 0x00b0f4,
footer: { text: 'WOS 쿠폰 자동 크롤러 | whiteoutsurvival.wiki' },
timestamp: new Date().toISOString(),
},
],
};
try {
await axios.post(webhookUrl, payload, { timeout: 5000 });
this.logger.log(`Discord 알림 전송 완료 (${codes.length}개 쿠폰)`);
} catch (err: any) {
this.logger.error(`Discord 알림 실패: ${err.message}`);
}
}
@Cron('0 0 * * * *')
async scheduledCrawl() {
await this.crawlWiki();
}
}