From a86bf218e52a17c1cfe2973e64d81ab32dfd9448 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 17:05:51 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20feat:=20access=5Ftoken=EC=9D=84?= =?UTF-8?q?=20=EB=B0=9C=EA=B8=89=EB=B0=9B=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 오늘의 상/하위 종목 조회를 위해 필요한 access_token 발급 로직 구현 --- BE/src/stocks/topfive/topfive.service.ts | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 BE/src/stocks/topfive/topfive.service.ts diff --git a/BE/src/stocks/topfive/topfive.service.ts b/BE/src/stocks/topfive/topfive.service.ts new file mode 100644 index 00000000..d801e213 --- /dev/null +++ b/BE/src/stocks/topfive/topfive.service.ts @@ -0,0 +1,43 @@ +import axios from 'axios'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class TopFiveService { + private accessToken: string; + private tokenExpireTime: Date; + private readonly koreaInvestmentConfig: { + appKey: string; + appSecret: string; + baseUrl: string; + }; + + constructor(private readonly config: ConfigService) { + this.koreaInvestmentConfig = { + appKey: this.config.get('KOREA_INVESTMENT_APP_KEY'), + appSecret: this.config.get('KOREA_INVESTMENT_APP_SECRET'), + baseUrl: this.config.get('KOREA_INVESTMENT_BASE_URL'), + }; + } + + private async getAccessToken() { + // accessToken이 유효한 경우 + if (this.accessToken && this.tokenExpireTime > new Date()) { + return this.accessToken; + } + + const response = await axios.post( + `${this.koreaInvestmentConfig.baseUrl}/oauth2/tokenP`, + { + grant_type: 'client_credentials', + appkey: this.koreaInvestmentConfig.appKey, + appsecret: this.koreaInvestmentConfig.appSecret, + }, + ); + + this.accessToken = response.data.access_token; + this.tokenExpireTime = new Date(Date.now() + +response.data.expires_in); + + return this.accessToken; + } +} From 414b03cfbfe3cff00a7bf6031d7709addb18fe33 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 17:07:40 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=98=A4=EB=8A=98?= =?UTF-8?q?=EC=9D=98=20=EC=83=81/=ED=95=98=EC=9C=84=20=EC=A2=85=EB=AA=A9?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20?= =?UTF-8?q?DTO=20=EA=B5=AC=ED=98=84#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 오늘의 상/하위 종목 조회를 위해 필요한 DTO 구현 --- .../topfive/dto/stock-ranking-data.dto.ts | 21 +++++++++++++++++ .../topfive/dto/stock-ranking-request.dto.ts | 23 +++++++++++++++++++ .../topfive/dto/stock-ranking-response.dto.ts | 13 +++++++++++ 3 files changed, 57 insertions(+) create mode 100644 BE/src/stocks/topfive/dto/stock-ranking-data.dto.ts create mode 100644 BE/src/stocks/topfive/dto/stock-ranking-request.dto.ts create mode 100644 BE/src/stocks/topfive/dto/stock-ranking-response.dto.ts diff --git a/BE/src/stocks/topfive/dto/stock-ranking-data.dto.ts b/BE/src/stocks/topfive/dto/stock-ranking-data.dto.ts new file mode 100644 index 00000000..5f1a3026 --- /dev/null +++ b/BE/src/stocks/topfive/dto/stock-ranking-data.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * 등락률 API 요청 후 받은 응답값 정제용 DTO + */ +export class StockRankingDataDto { + @ApiProperty({ description: 'HTS 한글 종목명' }) + hts_kor_isnm: string; + + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; +} diff --git a/BE/src/stocks/topfive/dto/stock-ranking-request.dto.ts b/BE/src/stocks/topfive/dto/stock-ranking-request.dto.ts new file mode 100644 index 00000000..200c244d --- /dev/null +++ b/BE/src/stocks/topfive/dto/stock-ranking-request.dto.ts @@ -0,0 +1,23 @@ +/** + * 등락률 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO + */ +export class StockRankigRequestDto { + /** + * 조건 시장 분류 코드 + * 'J' 주식 + */ + fid_cond_mrkt_div_code: string; + + /** + * 입력 종목 코드 + * '0000' 전체 / '0001' 코스피 + * '1001' 코스닥 / '2001' 코스피200 + */ + fid_input_iscd: string; + + /** + * 순위 정렬 구분 코드 + * '0' 상승률 / '1' 하락률 + */ + fid_rank_sort_cls_code: string; +} diff --git a/BE/src/stocks/topfive/dto/stock-ranking-response.dto.ts b/BE/src/stocks/topfive/dto/stock-ranking-response.dto.ts new file mode 100644 index 00000000..79a4954b --- /dev/null +++ b/BE/src/stocks/topfive/dto/stock-ranking-response.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StockRankingDataDto } from './stock-ranking-data.dto'; + +/** + * 순위 정렬 후 FE에 보낼 DTO + */ +export class StockRankingResponseDto { + @ApiProperty({ type: [StockRankingDataDto], description: '상승률 순위' }) + high: StockRankingDataDto[]; + + @ApiProperty({ type: [StockRankingDataDto], description: '하락률 순위' }) + low: StockRankingDataDto[]; +} From 2af0a7d3c50ea885df12159171de4fe812212749 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 17:09:15 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=95=9C=EA=B5=AD?= =?UTF-8?q?=ED=88=AC=EC=9E=90=20API=20=EC=9A=94=EC=B2=AD=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EA=B0=92=EC=9D=84=20=EB=B0=9B=EC=95=84?= =?UTF-8?q?=EC=98=A4=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 한국투자 Open API를 활용해 정보를 요청하고, 해당 정보를 모두 가져와 저장하는 로직 구현 --- BE/src/stocks/topfive/topfive.service.ts | 152 +++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/BE/src/stocks/topfive/topfive.service.ts b/BE/src/stocks/topfive/topfive.service.ts index d801e213..51a563ef 100644 --- a/BE/src/stocks/topfive/topfive.service.ts +++ b/BE/src/stocks/topfive/topfive.service.ts @@ -1,6 +1,50 @@ import axios from 'axios'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { StockRankigRequestDto } from './dto/stock-ranking-request.dto'; +import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; +import { StockRankingDataDto } from './dto/stock-ranking-data.dto'; + +export enum MarketType { + ALL = 'ALL', + KOSPI = 'KOSPI', + KOSDAQ = 'KOSDAQ', + KOSPI200 = 'KOSPI200', +} + +interface StockApiOutputData { + stck_shrn_iscd: string; + data_rank: string; + hts_kor_isnm: string; + stck_prpr: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + acml_vol: string; + stck_hgpr: string; + hgpr_hour: string; + acml_hgpr_data: string; + stck_lwpr: string; + lwpr_hour: string; + acml_lwpr_date: string; + lwpr_vrss_prpr_rate: string; + dsgt_date_clpr_vrss_prpr_rate: string; + cnnt_ascn_dynu: string; + hgpr_vrss_prpr_rate: string; + cnnt_down_dynu: string; + oprc_vrss_prpr_sign: string; + oprc_vrss_prpr: string; + oprc_vrss_prpr_rate: string; + prd_rsfl: string; + prd_rsfl_rate: string; +} + +interface StockApiResponse { + output: StockApiOutputData[]; + rt_cd: string; + msg_cd: string; + msg1: string; +} @Injectable() export class TopFiveService { @@ -40,4 +84,112 @@ export class TopFiveService { return this.accessToken; } + + private async requestApi(params: StockRankigRequestDto) { + try { + const token = await this.getAccessToken(); + + const response = await axios.get( + `${this.koreaInvestmentConfig.baseUrl}/uapi/domestic-stock/v1/ranking/fluctuation`, + { + headers: { + 'content-type': 'application/json; charset=utf-8', + authorization: `Bearer ${token}`, + appkey: this.koreaInvestmentConfig.appKey, + appsecret: this.koreaInvestmentConfig.appSecret, + tr_id: 'FHPST01700000', + custtype: 'P', + }, + params: { + fid_rsfl_rate2: '', + fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, + fid_cond_scr_div_code: '20170', + fid_input_iscd: params.fid_input_iscd, + fid_rank_sort_cls_code: params.fid_rank_sort_cls_code, + fid_input_cnt_1: '0', + fid_prc_cls_code: '1', + fid_input_price_1: '', + fid_input_price_2: '', + fid_vol_cnt: '', + fid_trgt_cls_code: '0', + fid_trgt_exls_cls_code: '0', + fid_div_cls_code: '0', + fid_rsfl_rate1: '', + }, + }, + ); + return response.data; + } catch (error) { + console.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + + async getMarketRanking(marketType: MarketType) { + try { + const params = new StockRankigRequestDto(); + params.fid_cond_mrkt_div_code = 'J'; + + switch (marketType) { + case MarketType.ALL: + params.fid_input_iscd = '0000'; + break; + case MarketType.KOSPI: + params.fid_input_iscd = '0001'; + break; + case MarketType.KOSDAQ: + params.fid_input_iscd = '1001'; + break; + case MarketType.KOSPI200: + params.fid_input_iscd = '2001'; + break; + default: + break; + } + + const highResponse = await this.requestApi({ + ...params, + fid_rank_sort_cls_code: '0', + }); + + const lowResponse = await this.requestApi({ + ...params, + fid_rank_sort_cls_code: '1', + }); + + const response = new StockRankingResponseDto(); + response.high = this.formatStockData(highResponse.output); + response.low = this.formatStockData(lowResponse.output); + + return response; + } catch (error) { + console.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + + private formatStockData(stocks: StockApiOutputData[]) { + return stocks.map((stock) => { + const stockData = new StockRankingDataDto(); + stockData.hts_kor_isnm = stock.hts_kor_isnm; + stockData.stck_prpr = stock.stck_prpr; + stockData.prdy_vrss = stock.prdy_vrss; + stockData.prdy_vrss_sign = stock.prdy_vrss_sign; + stockData.prdy_ctrt = stock.prdy_ctrt; + + return stockData; + }); + } } From f8c7046982da61436b7cd325de18f36a7e81c46e Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 17:10:16 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=98=A4=EB=8A=98?= =?UTF-8?q?=EC=9D=98=20=EC=83=81/=ED=95=98=EC=9C=84=20=EC=A2=85=EB=AA=A9?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API=20Controller=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stocks/topfive/topfive.controller.ts | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 BE/src/stocks/topfive/topfive.controller.ts diff --git a/BE/src/stocks/topfive/topfive.controller.ts b/BE/src/stocks/topfive/topfive.controller.ts new file mode 100644 index 00000000..9dcf2715 --- /dev/null +++ b/BE/src/stocks/topfive/topfive.controller.ts @@ -0,0 +1,28 @@ +import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { Controller, Get, Query } from '@nestjs/common'; +import { TopFiveService, MarketType } from './topfive.service'; +import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; + +@Controller('/api/stocks') +export class TopfiveController { + constructor(private readonly topFiveService: TopFiveService) {} + + @Get('topfive') + @ApiOperation({ summary: '오늘의 상/하위 종목 조회 API' }) + @ApiQuery({ + name: 'market', + enum: MarketType, + required: true, + description: + '주식 시장 구분\n' + + 'ALL: 전체, KOSPI: 코스피, KOSDAQ: 코스닥, KOSPI200: 코스피200', + }) + @ApiResponse({ + status: 200, + description: '주식 시장별 순위 조회 성공', + type: StockRankingResponseDto, + }) + async getTopFive(@Query('market') market: MarketType) { + return this.topFiveService.getMarketRanking(market); + } +} From ba8e2d621355c4ae7e90de75ceb40a47a6cab272 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 17:13:29 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20TopFiveModule?= =?UTF-8?q?=EC=9D=84=20AppModule=EC=97=90=20=EC=B6=94=EA=B0=80#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 ++ BE/src/stocks/topfive/topfive.module.ts | 11 +++++++++++ 2 files changed, 13 insertions(+) create mode 100644 BE/src/stocks/topfive/topfive.module.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index db5337d5..efa0165f 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { TopfiveModule } from './stocks/topfive/topfive.module'; @Module({ imports: [ @@ -17,6 +18,7 @@ import { AppService } from './app.service'; entities: [], synchronize: true, }), + TopfiveModule, ], controllers: [AppController], providers: [AppService], diff --git a/BE/src/stocks/topfive/topfive.module.ts b/BE/src/stocks/topfive/topfive.module.ts new file mode 100644 index 00000000..70f1d6b9 --- /dev/null +++ b/BE/src/stocks/topfive/topfive.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { TopfiveController } from './topfive.controller'; +import { TopFiveService } from './topfive.service'; + +@Module({ + imports: [ConfigModule], + controllers: [TopfiveController], + providers: [TopFiveService], +}) +export class TopfiveModule {} From 773642066bf07bcdb0b04d11a43500a791b0981a Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 17:24:46 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=83=81/=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EC=A2=85=EB=AA=A9=205=EA=B0=9C=EB=A7=8C=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stocks/topfive/topfive.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stocks/topfive/topfive.service.ts b/BE/src/stocks/topfive/topfive.service.ts index 51a563ef..7fdf49ed 100644 --- a/BE/src/stocks/topfive/topfive.service.ts +++ b/BE/src/stocks/topfive/topfive.service.ts @@ -181,7 +181,7 @@ export class TopFiveService { } private formatStockData(stocks: StockApiOutputData[]) { - return stocks.map((stock) => { + return stocks.slice(0, 5).map((stock) => { const stockData = new StockRankingDataDto(); stockData.hts_kor_isnm = stock.hts_kor_isnm; stockData.stck_prpr = stock.stck_prpr; From 3c1ffcb0b2e5188f0a8c418884618ef44287fff9 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 18:37:05 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=92=A1=20comment:=20Console.error=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B2=98=EB=A6=AC#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Github Actions에서 warning으로 처리되어 임시 주석처리 --- BE/src/stocks/topfive/topfive.service.ts | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/BE/src/stocks/topfive/topfive.service.ts b/BE/src/stocks/topfive/topfive.service.ts index 7fdf49ed..404aaf19 100644 --- a/BE/src/stocks/topfive/topfive.service.ts +++ b/BE/src/stocks/topfive/topfive.service.ts @@ -86,6 +86,7 @@ export class TopFiveService { } private async requestApi(params: StockRankigRequestDto) { + // eslint-disable-next-line no-useless-catch try { const token = await this.getAccessToken(); @@ -120,18 +121,19 @@ export class TopFiveService { ); return response.data; } catch (error) { - console.error('API Error Details:', { - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - headers: error.response?.config?.headers, // 실제 요청 헤더 - message: error.message, - }); + // console.error('API Error Details:', { + // status: error.response?.status, + // statusText: error.response?.statusText, + // data: error.response?.data, + // headers: error.response?.config?.headers, // 실제 요청 헤더 + // message: error.message, + // }); throw error; } } async getMarketRanking(marketType: MarketType) { + // eslint-disable-next-line no-useless-catch try { const params = new StockRankigRequestDto(); params.fid_cond_mrkt_div_code = 'J'; @@ -169,13 +171,13 @@ export class TopFiveService { return response; } catch (error) { - console.error('API Error Details:', { - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - headers: error.response?.config?.headers, // 실제 요청 헤더 - message: error.message, - }); + // console.error('API Error Details:', { + // status: error.response?.status, + // statusText: error.response?.statusText, + // data: error.response?.data, + // headers: error.response?.config?.headers, // 실제 요청 헤더 + // message: error.message, + // }); throw error; } } From 4375e5f5de4dfc5f4fd75d8cabae2ad750f35b4a Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 6 Nov 2024 19:08:14 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=ED=96=88=EB=8D=98=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20logger=EB=A1=9C=20=EB=B3=80=EA=B2=BD#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stocks/topfive/topfive.service.ts | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/BE/src/stocks/topfive/topfive.service.ts b/BE/src/stocks/topfive/topfive.service.ts index 404aaf19..f2197c58 100644 --- a/BE/src/stocks/topfive/topfive.service.ts +++ b/BE/src/stocks/topfive/topfive.service.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { StockRankigRequestDto } from './dto/stock-ranking-request.dto'; import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; @@ -56,6 +56,8 @@ export class TopFiveService { baseUrl: string; }; + private readonly logger = new Logger(); + constructor(private readonly config: ConfigService) { this.koreaInvestmentConfig = { appKey: this.config.get('KOREA_INVESTMENT_APP_KEY'), @@ -86,7 +88,6 @@ export class TopFiveService { } private async requestApi(params: StockRankigRequestDto) { - // eslint-disable-next-line no-useless-catch try { const token = await this.getAccessToken(); @@ -121,19 +122,18 @@ export class TopFiveService { ); return response.data; } catch (error) { - // console.error('API Error Details:', { - // status: error.response?.status, - // statusText: error.response?.statusText, - // data: error.response?.data, - // headers: error.response?.config?.headers, // 실제 요청 헤더 - // message: error.message, - // }); + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, + message: error.message, + }); throw error; } } async getMarketRanking(marketType: MarketType) { - // eslint-disable-next-line no-useless-catch try { const params = new StockRankigRequestDto(); params.fid_cond_mrkt_div_code = 'J'; @@ -171,13 +171,13 @@ export class TopFiveService { return response; } catch (error) { - // console.error('API Error Details:', { - // status: error.response?.status, - // statusText: error.response?.statusText, - // data: error.response?.data, - // headers: error.response?.config?.headers, // 실제 요청 헤더 - // message: error.message, - // }); + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); throw error; } }