Skip to content

Commit

Permalink
Merge pull request #203 from boostcampwm-2024/feature/api/news-#181
Browse files Browse the repository at this point in the history
[BE] 10.03 뉴스 조회 API 구현 #181
  • Loading branch information
uuuo3o authored Nov 27, 2024
2 parents 286bbbc + 9ca729c commit 4b0dda9
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 0 deletions.
2 changes: 2 additions & 0 deletions BE/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { StockTradeHistoryModule } from './stock/trade/history/stock-trade-histo
import { RedisModule } from './common/redis/redis.module';
import { HTTPExceptionFilter } from './common/filters/http-exception.filter';
import { RankingModule } from './ranking/ranking.module';
import { NewsModule } from './news/news.module';
import { StockBookmarkModule } from './stock/bookmark/stock-bookmark.module';

@Module({
Expand All @@ -36,6 +37,7 @@ import { StockBookmarkModule } from './stock/bookmark/stock-bookmark.module';
StockTradeHistoryModule,
RedisModule,
RankingModule,
NewsModule,
StockBookmarkModule,
],
controllers: [AppController],
Expand Down
7 changes: 7 additions & 0 deletions BE/src/news/dto/news-data-output.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class NewsDataOutputDto {
title: string;
originallink: string;
link: string;
description: string;
pubDate: string;
}
24 changes: 24 additions & 0 deletions BE/src/news/dto/news-database-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';

export class NewsDatabaseResponseDto {
@ApiProperty({ description: '기본키' })
id: number;

@ApiProperty({ description: '뉴스 기사 제목' })
title: string;

@ApiProperty({ description: '원문 URL' })
originallink: string;

@ApiProperty({ description: '뉴스 기사의 내용을 요약한 패시지 정보' })
description: string;

@ApiProperty({ description: '기사 원문이 제공된 시간' })
pubDate: string;

@ApiProperty({ description: '검색 키워드' })
query: string;

@ApiProperty({ description: 'cron 동작 시간' })
updatedAt: Date;
}
18 changes: 18 additions & 0 deletions BE/src/news/dto/news-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';

export class NewsResponseDto {
@ApiProperty({ description: '뉴스 기사 제목' })
title: string;

@ApiProperty({ description: '원문 URL' })
originallink: string;

@ApiProperty({ description: '뉴스 기사의 내용을 요약한 패시지 정보' })
description: string;

@ApiProperty({ description: '기사 원문이 제공된 시간' })
pubDate: string;

@ApiProperty({ description: '검색 키워드' })
query: string;
}
9 changes: 9 additions & 0 deletions BE/src/news/interface/news-value.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NewsDataOutputDto } from '../dto/news-data-output.dto';

export interface NewsApiResponse {
lastBuildDate: string;
total: number;
start: number;
display: number;
items: NewsDataOutputDto[];
}
27 changes: 27 additions & 0 deletions BE/src/news/naver-api-domian.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import axios from 'axios';
import { Injectable } from '@nestjs/common';

@Injectable()
export class NaverApiDomianService {
/**
* @private NAVER Developers Search API - API 호출용 공통 함수
* @param {Record<string, string>} params - API 요청 시 필요한 쿼리 파라미터 DTO
* @returns - API 호출에 대한 응답 데이터
*
* @author uuuo3o
*/
async requestApi<T>(params: Record<string, string | number>): Promise<T> {
const headers = {
'X-Naver-Client-Id': process.env.NAVER_CLIENT_ID,
'X-Naver-Client-Secret': process.env.NAVER_CLIENT_SECRET,
};
const url = 'https://openapi.naver.com/v1/search/news.json';

const response = await axios.get<T>(url, {
headers,
params,
});

return response.data;
}
}
20 changes: 20 additions & 0 deletions BE/src/news/news.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Controller, Get } from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { NewsService } from './news.service';
import { NewsDatabaseResponseDto } from './dto/news-database-response.dto';

@ApiTags('뉴스 API')
@Controller('/api/news')
export class NewsController {
constructor(private readonly newsService: NewsService) {}

@Get()
@ApiResponse({
status: 200,
description: '뉴스 조회 성공',
type: [NewsDatabaseResponseDto],
})
async getNews() {
return this.newsService.getNews();
}
}
30 changes: 30 additions & 0 deletions BE/src/news/news.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
UpdateDateColumn,
} from 'typeorm';

@Entity('news')
export class News {
@PrimaryGeneratedColumn()
id: number;

@Column()
title: string;

@Column()
originallink: string;

@Column()
description: string;

@Column({ name: 'pub_date' })
pubDate: Date;

@Column()
query: string;

@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
14 changes: 14 additions & 0 deletions BE/src/news/news.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NaverApiDomianService } from './naver-api-domian.service';
import { NewsController } from './news.controller';
import { NewsService } from './news.service';
import { NewsRepository } from './news.repository';
import { News } from './news.entity';

@Module({
imports: [TypeOrmModule.forFeature([News])],
controllers: [NewsController],
providers: [NewsService, NaverApiDomianService, NewsRepository],
})
export class NewsModule {}
11 changes: 11 additions & 0 deletions BE/src/news/news.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { News } from './news.entity';

@Injectable()
export class NewsRepository extends Repository<News> {
constructor(@InjectDataSource() private readonly dataSource: DataSource) {
super(News, dataSource.createEntityManager());
}
}
60 changes: 60 additions & 0 deletions BE/src/news/news.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { In } from 'typeorm';
import { NaverApiDomianService } from './naver-api-domian.service';
import { NewsApiResponse } from './interface/news-value.interface';
import { NewsDataOutputDto } from './dto/news-data-output.dto';
import { NewsResponseDto } from './dto/news-response.dto';
import { NewsRepository } from './news.repository';

@Injectable()
export class NewsService {
constructor(
private readonly naverApiDomainService: NaverApiDomianService,
private readonly newsRepository: NewsRepository,
) {}

async getNews() {
return this.newsRepository.find();
}

@Cron('*/30 8-16 * * 1-5')
async cronNewsData() {
await this.newsRepository.delete({ query: In(['증권', '주식']) });
await this.getNewsDataByQuery('주식');
await this.getNewsDataByQuery('증권');

await this.newsRepository.update(
{},
{
updatedAt: new Date(),
},
);
}

private async getNewsDataByQuery(value: string) {
const queryParams = {
query: value,
};

const response =
await this.naverApiDomainService.requestApi<NewsApiResponse>(queryParams);
const formattedData = this.formatNewsData(value, response.items);

return this.newsRepository.save(formattedData);
}

private formatNewsData(query: string, items: NewsDataOutputDto[]) {
return items.slice(0, 10).map((item) => {
const result = new NewsResponseDto();

result.title = item.title.replace(/<\/?b>/g, '');
result.description = item.description.replace(/<\/?b>/g, '');
result.originallink = item.originallink;
result.pubDate = item.pubDate;
result.query = query;

return result;
});
}
}

0 comments on commit 4b0dda9

Please sign in to comment.