forked from boostcampwm-2024/web17-juchumjuchum
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #46 from boostcampwm-2024/task-#45-newsAI-cicd
Task #45 news ai cicd
- Loading branch information
Showing
13 changed files
with
899 additions
and
1,348 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
packages/backend/src/news/StockNewsOrchestrationService.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import { Logger } from 'winston'; | ||
import { NewsCrawlingService } from '@/news/newsCrawling.service'; | ||
import { NewsSummaryService } from '@/news/newsSummary.service'; | ||
import { StockNewsRepository } from '@/news/stockNews.repository'; | ||
import { CrawlingDataDto } from '@/news/dto/crawlingData.dto'; | ||
import { Cron } from '@nestjs/schedule'; | ||
import { CreateStockNewsDto } from '@/news/dto/stockNews.dto'; | ||
|
||
@Injectable() | ||
export class StockNewsOrchestrationService { | ||
constructor( | ||
@Inject('winston') private readonly logger: Logger, | ||
private readonly newsCrawlingService: NewsCrawlingService, | ||
private readonly newsSummaryService: NewsSummaryService, | ||
private readonly stockNewsRepository: StockNewsRepository, | ||
) {} | ||
|
||
@Cron('19 15 0 * * *') //오후 3시 19분 | ||
public async orchestrateStockProcessing() { | ||
const stockNameList = [ | ||
'삼성전자', | ||
'SK하이닉스', | ||
'LG에너지솔루션', | ||
'삼성바이오로직스', | ||
'현대차', | ||
'기아', | ||
'셀트리온', | ||
'NAVER', | ||
'KB금융', | ||
'HD현대중공업', | ||
]; | ||
// 주식 데이터 크롤링 | ||
for (const stockName of stockNameList) { | ||
const stockDataList = | ||
await this.newsCrawlingService.getNewsLinks(stockName); | ||
|
||
const stockNewsData: CrawlingDataDto = | ||
await this.newsCrawlingService.crawling( | ||
stockDataList!.stock, | ||
stockDataList!.response, | ||
); | ||
// 데이터 요약 | ||
const summarizedData = | ||
await this.newsSummaryService.summarizeNews(stockNewsData); | ||
|
||
console.log('summarizedData'); | ||
console.log(summarizedData); | ||
|
||
// DB에 저장 | ||
if (summarizedData) { | ||
await this.stockNewsRepository.create(summarizedData); | ||
} else { | ||
this.logger.error('Failed to summarize news'); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export class CrawlNewsItemDto { | ||
date: string; | ||
title: string; | ||
content: string; | ||
url: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { CrawlNewsItemDto } from './crawlNewsItem.dto'; | ||
|
||
export class CrawlingDataDto { | ||
stockName: string; | ||
news: CrawlNewsItemDto[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { NewsItemDto } from './newsItemDto'; | ||
|
||
export class NewsInfoDto { | ||
lastBuildDate: string; | ||
total: number; | ||
start: number; | ||
display: number; | ||
items: NewsItemDto[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export class NewsItemDto { | ||
title: string; | ||
original_link: string; | ||
link: string; | ||
description: string; | ||
pubDate: Date; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import axios from 'axios'; | ||
import * as cheerio from 'cheerio'; | ||
import { Logger } from 'winston'; | ||
import { NewsInfoDto } from './dto/newsInfoDto'; | ||
import { NewsItemDto } from './dto/newsItemDto'; | ||
import { CrawlingDataDto } from '@/news/dto/crawlingData.dto'; | ||
|
||
@Injectable() | ||
export class NewsCrawlingService { | ||
constructor(@Inject('winston') private readonly logger: Logger) { | ||
} | ||
|
||
// naver news API 이용해 뉴스 정보 얻어오기 | ||
async getNewsLinks(stockName: string) { | ||
const encodedStockName = encodeURI(stockName); | ||
const newsUrl = `${process.env.NAVER_NEWS_URL}?query=${encodedStockName}&display=10&sort=sim`; | ||
try { | ||
const res: NewsInfoDto = await axios(newsUrl, { | ||
method: 'GET', | ||
headers: { | ||
'X-Naver-Client-Id': process.env.NAVER_CLIENT_ID, | ||
'X-Naver-Client-Secret': process.env.NAVER_CLIENT_SECRET, | ||
}, | ||
}).then((r) => r.data); | ||
return { | ||
stock: stockName, | ||
response: await this.extractNaverNews(res), | ||
}; | ||
} catch (err) { | ||
this.logger.error(err); | ||
} | ||
} | ||
|
||
async extractNaverNews(newsData: NewsInfoDto) { | ||
return newsData.items.filter((e) => e.link.includes('n.news.naver.com')); | ||
} | ||
|
||
// 얻어온 뉴스 정보들 중 naver news에 기사가 있는 사이트에서 제목, 본문, 생성 날짜등을 크롤링해오기 | ||
async crawling(stock: string, news: NewsItemDto[]) { | ||
return { | ||
stockName: stock, | ||
news: await Promise.all( | ||
news.map(async (n) => { | ||
const url = decodeURI(n.link); | ||
return await axios(url, { | ||
method: 'GET', | ||
headers: { | ||
'User-Agent': | ||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3', | ||
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7', | ||
}, | ||
}).then(async (r) => { | ||
const htmlString = await r.data; | ||
const $ = cheerio.load(htmlString); | ||
|
||
const date = $('span._ARTICLE_DATE_TIME').attr('data-date-time'); | ||
const title = $('#title_area').text(); | ||
const content = $('#dic_area').text(); | ||
return { | ||
date: date, | ||
title: title, | ||
content: content, | ||
url: url, | ||
}; | ||
}); | ||
}), | ||
), | ||
} as CrawlingDataDto; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,42 @@ | ||
import { Controller, Post, Body, Get, Param } from '@nestjs/common'; | ||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; | ||
import { StockNewsService } from '@/news/stockNews.service'; | ||
import { CreateStockNewsDto, StockNewsResponse } from '@/news/dto/stockNews.dto'; | ||
import { StockNewsRepository } from '@/news/stockNews.repository'; | ||
import { StockNewsOrchestrationService } from '@/news/StockNewsOrchestrationService'; | ||
|
||
@ApiTags('Stock News') | ||
@Controller('stock/news') | ||
export class StockNewsController { | ||
constructor(private readonly stockNewsService: StockNewsService) {} | ||
constructor( | ||
private readonly stockNewsRepository: StockNewsRepository, | ||
private readonly stockNewsOrchestrationService: StockNewsOrchestrationService) {} | ||
|
||
@Post() | ||
@ApiOperation({ summary: '주식 뉴스 정보 저장' }) | ||
@ApiResponse({ status: 201, type: StockNewsResponse }) | ||
async create(@Body() createStockNewsDto: CreateStockNewsDto) { | ||
const stockNews = await this.stockNewsService.create(createStockNewsDto); | ||
const stockNews = await this.stockNewsRepository.create(createStockNewsDto); | ||
return new StockNewsResponse(stockNews); | ||
} | ||
|
||
@Get(':stockId') | ||
@ApiOperation({ summary: '종목별 뉴스 조회' }) | ||
@ApiResponse({ status: 200, type: [StockNewsResponse] }) | ||
async findByStockId(@Param('stockId') stockId: string) { | ||
const newsList = await this.stockNewsService.findByStockId(stockId); | ||
return newsList.map(news => new StockNewsResponse(news)); | ||
const newsList = await this.stockNewsRepository.findByStockId(stockId); | ||
return newsList.map((news) => new StockNewsResponse(news)); | ||
} | ||
|
||
@Get(':stockId/latest') | ||
@ApiOperation({ summary: '종목별 최신 뉴스 조회' }) | ||
@ApiResponse({ status: 200, type: StockNewsResponse }) | ||
async findLatestByStockId(@Param('stockId') stockId: string) { | ||
const news = await this.stockNewsService.findLatestByStockId(stockId); | ||
const news = await this.stockNewsRepository.findLatestByStockId(stockId); | ||
return news ? new StockNewsResponse(news) : null; | ||
} | ||
|
||
@Post('test') | ||
async testAI(){ | ||
await this.stockNewsOrchestrationService.orchestrateStockProcessing(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,16 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { StockNews } from '@/news/domain/stockNews.entity'; | ||
import { StockNewsController } from '@/news/stockNews.controller'; | ||
import { Module } from '@nestjs/common'; | ||
import { StockNewsService } from '@/news/stockNews.service'; | ||
import { StockNewsRepository } from '@/news/stockNews.repository'; | ||
import { NewsCrawlingService } from '@/news/newsCrawling.service'; | ||
import { StockNewsOrchestrationService } from '@/news/StockNewsOrchestrationService'; | ||
import { NewsSummaryService } from '@/news/newsSummary.service'; | ||
|
||
@Module({ | ||
imports: [TypeOrmModule.forFeature([StockNews])], | ||
controllers: [StockNewsController], | ||
providers: [StockNewsService], | ||
exports: [StockNewsService], | ||
providers: [StockNewsRepository, NewsCrawlingService, StockNewsOrchestrationService, NewsSummaryService], | ||
exports: [], | ||
}) | ||
|
||
export class StockNewsModule {} | ||
export class StockNewsModule {} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.