Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 10.03 뉴스 조회 API 구현 #181 #203

Merged
merged 17 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
});
}
}
Loading