diff --git a/back/.env.example b/back/.env.example index 9eeda6c..1d90ced 100644 --- a/back/.env.example +++ b/back/.env.example @@ -5,3 +5,5 @@ DB_PASSWORD=486251daKWON@ DB_DATABASE=kakao APP_PORT=3000 DISCORD_WEBHOOK_URL= +REDDIT_CLIENT_ID= +REDDIT_CLIENT_SECRET= diff --git a/back/src/crawler/crawler.service.ts b/back/src/crawler/crawler.service.ts index 00b6611..c5c9148 100644 --- a/back/src/crawler/crawler.service.ts +++ b/back/src/crawler/crawler.service.ts @@ -6,8 +6,8 @@ import axios from 'axios'; import { WosCoupon } from '../entities/wos-coupon.entity'; import { WosService } from '../wos/wos.service'; -const REDDIT_SEARCH_URL = - 'https://www.reddit.com/r/WhiteoutSurvival/search.json?q=gift+code+OR+redeem+code+OR+coupon&sort=new&restrict_sr=1&limit=25&t=week'; +const REDDIT_OAUTH_URL = 'https://oauth.reddit.com/r/WhiteoutSurvival/search.json?q=gift+code+OR+redeem+code+OR+coupon&sort=new&restrict_sr=1&limit=25&t=week'; +const REDDIT_TOKEN_URL = 'https://www.reddit.com/api/v1/access_token'; const COMMON_WORDS = new Set([ 'THE', 'AND', 'FOR', 'ARE', 'BUT', 'NOT', 'YOU', 'ALL', 'ANY', 'CAN', @@ -33,6 +33,8 @@ const COMMON_WORDS = new Set([ export class CrawlerService { private readonly logger = new Logger(CrawlerService.name); private seenPostIds = new Set(); + private redditAccessToken: string | null = null; + private redditTokenExpiry = 0; constructor( @InjectRepository(WosCoupon) @@ -40,6 +42,34 @@ export class CrawlerService { private readonly wosService: WosService, ) {} + private async getRedditToken(): Promise { + const clientId = process.env.REDDIT_CLIENT_ID; + const clientSecret = process.env.REDDIT_CLIENT_SECRET; + if (!clientId || !clientSecret) return null; + + if (this.redditAccessToken && Date.now() < this.redditTokenExpiry) { + return this.redditAccessToken; + } + + try { + const resp = await axios.post( + REDDIT_TOKEN_URL, + new URLSearchParams({ grant_type: 'client_credentials' }), + { + auth: { username: clientId, password: clientSecret }, + headers: { 'User-Agent': 'WosCouponBot/1.0 by wageulwageul' }, + timeout: 10000, + }, + ); + this.redditAccessToken = resp.data.access_token; + this.redditTokenExpiry = Date.now() + (resp.data.expires_in - 60) * 1000; + return this.redditAccessToken; + } catch (err: any) { + this.logger.error(`Reddit 토큰 발급 실패: ${err.message}`); + return null; + } + } + private extractCodes(text: string): string[] { const codePattern = /\b([A-Z][A-Z0-9]{4,19})\b/g; const found = new Set(); @@ -97,9 +127,16 @@ export class CrawlerService { let posts: any[] = []; try { - const resp = await axios.get(REDDIT_SEARCH_URL, { + const token = await this.getRedditToken(); + if (!token) { + this.logger.warn('Reddit OAuth 토큰 없음 - REDDIT_CLIENT_ID/SECRET 환경변수를 설정하세요'); + return { found: 0, added: [] }; + } + + const resp = await axios.get(REDDIT_OAUTH_URL, { headers: { - 'User-Agent': 'WosCouponBot/1.0', + 'Authorization': `Bearer ${token}`, + 'User-Agent': 'WosCouponBot/1.0 by wageulwageul', 'Accept': 'application/json', }, timeout: 10000,