diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock.index.service.ts index f466a461..875be12d 100644 --- a/BE/src/stock/index/stock.index.service.ts +++ b/BE/src/stock/index/stock.index.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import axios from 'axios'; import { StockIndexListChartElementDto } from './dto/stock.index.list.chart.element.dto'; import { StockIndexListElementDto } from './dto/stock.index.list.element.dto'; import { StockIndexValueElementDto } from './dto/stock.index.value.element.dto'; @@ -10,23 +11,13 @@ import { @Injectable() export class StockIndexService { async getDomesticStockIndexListByCode(code: string, accessToken: string) { - const url = `${process.env.KOREA_INVESTMENT_BASE_URL}/uapi/domestic-stock/v1/quotations/inquire-index-timeprice`; - const queryParams = `?FID_INPUT_HOUR_1=300&FID_COND_MRKT_DIV_CODE=U&FID_INPUT_ISCD=${code}`; - - const response = await fetch(url + queryParams, { - method: 'GET', - headers: { - 'content-type': 'application/json; charset=utf-8', - authorization: `Bearer ${accessToken}`, - appkey: process.env.KOREA_INVESTMENT_APP_KEY, - appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, - tr_id: 'FHPUP02110200', - custtype: 'P', - }, - }); + const result = await this.requestDomesticStockIndexListApi( + code, + accessToken, + ); - const result: StockIndexChartInterface = await response.json(); - if (result.rt_cd !== '0') throw new Error('유효하지 않은 토큰'); + if (result.rt_cd !== '0') + throw new Error('데이터를 정상적으로 조회하지 못했습니다.'); return new StockIndexListElementDto( code, @@ -40,28 +31,71 @@ export class StockIndexService { } async getDomesticStockIndexValueByCode(code: string, accessToken: string) { - const url = `${process.env.KOREA_INVESTMENT_BASE_URL}/uapi/domestic-stock/v1/quotations/inquire-index-price`; - const queryParams = `?FID_COND_MRKT_DIV_CODE=U&FID_INPUT_ISCD=${code}`; + const result = await this.requestDomesticStockIndexValueApi( + code, + accessToken, + ); - const response = await fetch(url + queryParams, { - method: 'GET', - headers: { - 'content-type': 'application/json; charset=utf-8', - authorization: `Bearer ${accessToken}`, - appkey: process.env.KOREA_INVESTMENT_APP_KEY, - appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, - tr_id: 'FHPUP02100000', - custtype: 'P', - }, - }); + if (result.rt_cd !== '0') + throw new Error('데이터를 정상적으로 조회하지 못했습니다.'); - const result: StockIndexValueInterface = await response.json(); return new StockIndexValueElementDto( code, result.output.bstp_nmix_prpr, result.output.bstp_nmix_prdy_vrss, - result.output.bstp_nmix_prdy_vrss, + result.output.bstp_nmix_prdy_ctrt, result.output.prdy_vrss_sign, ); } + + private async requestDomesticStockIndexListApi( + code: string, + accessToken: string, + ) { + const response = await axios.get( + `${process.env.KOREA_INVESTMENT_BASE_URL}/uapi/domestic-stock/v1/quotations/inquire-index-timeprice`, + { + headers: { + 'content-type': 'application/json; charset=utf-8', + authorization: `Bearer ${accessToken}`, + appkey: process.env.KOREA_INVESTMENT_APP_KEY, + appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, + tr_id: 'FHPUP02110200', + custtype: 'P', + }, + params: { + fid_input_hour_1: 300, + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }, + }, + ); + + return response.data; + } + + private async requestDomesticStockIndexValueApi( + code: string, + accessToken: string, + ) { + const response = await axios.get( + `${process.env.KOREA_INVESTMENT_BASE_URL}/uapi/domestic-stock/v1/quotations/inquire-index-price`, + { + headers: { + 'content-type': 'application/json; charset=utf-8', + authorization: `Bearer ${accessToken}`, + appkey: process.env.KOREA_INVESTMENT_APP_KEY, + appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, + tr_id: 'FHPUP02100000', + custtype: 'P', + }, + params: { + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }, + }, + ); + + return response.data; + } } diff --git a/BE/src/websocket/interface/socket.interface.ts b/BE/src/websocket/interface/socket.interface.ts new file mode 100644 index 00000000..d9a6aabb --- /dev/null +++ b/BE/src/websocket/interface/socket.interface.ts @@ -0,0 +1,3 @@ +export interface SocketConnectTokenInterface { + approval_key: string; +} diff --git a/BE/src/websocket/socket.service.ts b/BE/src/websocket/socket.service.ts index 543517c0..0c25bf03 100644 --- a/BE/src/websocket/socket.service.ts +++ b/BE/src/websocket/socket.service.ts @@ -1,7 +1,9 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { WebSocket } from 'ws'; +import axios from 'axios'; import { SocketGateway } from './socket.gateway'; import { StockIndexValueElementDto } from '../stock/index/dto/stock.index.value.element.dto'; +import { SocketConnectTokenInterface } from './interface/socket.interface'; @Injectable() export class SocketService implements OnModuleInit { @@ -50,20 +52,16 @@ export class SocketService implements OnModuleInit { } private async getSocketConnectionKey() { - const url = `${process.env.KOREA_INVESTMENT_BASE_URL}/oauth2/Approval`; - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: JSON.stringify({ + const response = await axios.post( + `${process.env.KOREA_INVESTMENT_BASE_URL}/oauth2/Approval`, + { grant_type: 'client_credentials', appkey: process.env.KOREA_INVESTMENT_APP_KEY, secretkey: process.env.KOREA_INVESTMENT_APP_SECRET, - }), - }); - const result: SocketConnectTokenInterface = await response.json(); + }, + ); + + const result = response.data; return result.approval_key; } @@ -86,9 +84,3 @@ export class SocketService implements OnModuleInit { ); } } - -// interfaces - -interface SocketConnectTokenInterface { - approval_key: string; -} diff --git a/BE/test/stock/index/mockdata/stock.index.list.mockdata.ts b/BE/test/stock/index/mockdata/stock.index.list.mockdata.ts new file mode 100644 index 00000000..7fb74649 --- /dev/null +++ b/BE/test/stock/index/mockdata/stock.index.list.mockdata.ts @@ -0,0 +1,29 @@ +export const STOCK_INDEX_LIST_MOCK = { + VALID_DATA: { + data: { + output: [ + { + bsop_hour: '100600', + bstp_nmix_prpr: '916.77', + bstp_nmix_prdy_vrss: '11.27', + prdy_vrss_sign: '2', + bstp_nmix_prdy_ctrt: '1.24', + acml_tr_pbmn: '3839797', + acml_vol: '313374', + cntg_vol: '870', + }, + ], + rt_cd: '0', + msg_cd: 'MCA00000', + msg1: '정상처리 되었습니다.', + }, + }, + INVALID_DATA: { + data: { + output: [], + rt_cd: '1', + msg_cd: 'MCA00000', + msg1: '유효하지 않은 토큰입니다.', + }, + }, +}; diff --git a/BE/test/stock/index/mockdata/stock.index.value.mockdata.ts b/BE/test/stock/index/mockdata/stock.index.value.mockdata.ts new file mode 100644 index 00000000..0faa49d9 --- /dev/null +++ b/BE/test/stock/index/mockdata/stock.index.value.mockdata.ts @@ -0,0 +1,55 @@ +export const STOCK_INDEX_VALUE_MOCK = { + VALID_DATA: { + data: { + output: { + bstp_nmix_prpr: '857.60', + bstp_nmix_prdy_vrss: '-1.61', + prdy_vrss_sign: '5', + bstp_nmix_prdy_ctrt: '-0.19', + acml_vol: '1312496', + prdy_vol: '1222188', + acml_tr_pbmn: '11507962', + prdy_tr_pbmn: '11203385', + bstp_nmix_oprc: '863.69', + prdy_nmix_vrss_nmix_oprc: '4.48', + oprc_vrss_prpr_sign: '2', + bstp_nmix_oprc_prdy_ctrt: '0.52', + bstp_nmix_hgpr: '864.24', + prdy_nmix_vrss_nmix_hgpr: '5.03', + hgpr_vrss_prpr_sign: '2', + bstp_nmix_hgpr_prdy_ctrt: '0.59', + bstp_nmix_lwpr: '854.72', + prdy_clpr_vrss_lwpr: '-4.49', + lwpr_vrss_prpr_sign: '5', + prdy_clpr_vrss_lwpr_rate: '-0.52', + ascn_issu_cnt: '828', + uplm_issu_cnt: '5', + stnr_issu_cnt: '94', + down_issu_cnt: '716', + lslm_issu_cnt: '1', + dryy_bstp_nmix_hgpr: '890.06', + dryy_hgpr_vrss_prpr_rate: '3.65', + dryy_bstp_nmix_hgpr_date: '20240109', + dryy_bstp_nmix_lwpr: '786.28', + dryy_lwpr_vrss_prpr_rate: '-9.07', + dryy_bstp_nmix_lwpr_date: '20240201', + total_askp_rsqn: '24146999', + total_bidp_rsqn: '40450437', + seln_rsqn_rate: '37.38', + shnu_rsqn_rate: '62.62', + ntby_rsqn: '16303438', + }, + rt_cd: '0', + msg_cd: 'MCA00000', + msg1: '정상처리 되었습니다.', + }, + }, + INVALID_DATA: { + data: { + output: {}, + rt_cd: '1', + msg_cd: 'MCA00000', + msg1: '유효하지 않은 토큰입니다.', + }, + }, +}; diff --git a/BE/test/stock/index/stock.index.list.e2e-spec.ts b/BE/test/stock/index/stock.index.list.e2e-spec.ts new file mode 100644 index 00000000..7518b75d --- /dev/null +++ b/BE/test/stock/index/stock.index.list.e2e-spec.ts @@ -0,0 +1,49 @@ +import { Test } from '@nestjs/testing'; +import axios from 'axios'; +import { StockIndexService } from '../../../src/stock/index/stock.index.service'; +import { STOCK_INDEX_LIST_MOCK } from './mockdata/stock.index.list.mockdata'; + +jest.mock('axios'); + +describe('stock index list test', () => { + let stockIndexService: StockIndexService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [StockIndexService], + }).compile(); + + stockIndexService = module.get(StockIndexService); + }); + + it('주가 지수 차트 조회 API에서 정상적인 데이터를 조회한 경우, 형식에 맞춰 정상적으로 반환한다.', async () => { + (axios.get as jest.Mock).mockResolvedValue( + STOCK_INDEX_LIST_MOCK.VALID_DATA, + ); + + expect( + await stockIndexService.getDomesticStockIndexListByCode( + 'code', + 'accessToken', + ), + ).toEqual({ + code: 'code', + chart: [ + { + time: STOCK_INDEX_LIST_MOCK.VALID_DATA.data.output[0].bsop_hour, + value: STOCK_INDEX_LIST_MOCK.VALID_DATA.data.output[0].bstp_nmix_prpr, + }, + ], + }); + }); + + it('주가 지수 차트 조회 API에서 데이터를 조회하지 못한 경우, 에러를 발생시킨다.', async () => { + (axios.get as jest.Mock).mockResolvedValue( + STOCK_INDEX_LIST_MOCK.INVALID_DATA, + ); + + await expect( + stockIndexService.getDomesticStockIndexListByCode('code', 'accessToken'), + ).rejects.toThrow('데이터를 정상적으로 조회하지 못했습니다.'); + }); +}); diff --git a/BE/test/stock/index/stock.index.value.e2e-spec.ts b/BE/test/stock/index/stock.index.value.e2e-spec.ts new file mode 100644 index 00000000..12ff2590 --- /dev/null +++ b/BE/test/stock/index/stock.index.value.e2e-spec.ts @@ -0,0 +1,48 @@ +import { Test } from '@nestjs/testing'; +import axios from 'axios'; +import { StockIndexService } from '../../../src/stock/index/stock.index.service'; +import { STOCK_INDEX_VALUE_MOCK } from './mockdata/stock.index.value.mockdata'; + +jest.mock('axios'); + +describe('stock index list test', () => { + let stockIndexService: StockIndexService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [StockIndexService], + }).compile(); + + stockIndexService = module.get(StockIndexService); + }); + + it('주가 지수 값 조회 API에서 정상적인 데이터를 조회한 경우, 형식에 맞춰 정상적으로 반환한다.', async () => { + (axios.get as jest.Mock).mockResolvedValue( + STOCK_INDEX_VALUE_MOCK.VALID_DATA, + ); + + expect( + await stockIndexService.getDomesticStockIndexValueByCode( + 'code', + 'accessToken', + ), + ).toEqual({ + code: 'code', + value: STOCK_INDEX_VALUE_MOCK.VALID_DATA.data.output.bstp_nmix_prpr, + diff: STOCK_INDEX_VALUE_MOCK.VALID_DATA.data.output.bstp_nmix_prdy_vrss, + diffRate: + STOCK_INDEX_VALUE_MOCK.VALID_DATA.data.output.bstp_nmix_prdy_ctrt, + sign: STOCK_INDEX_VALUE_MOCK.VALID_DATA.data.output.prdy_vrss_sign, + }); + }); + + it('주가 지수 값 조회 API에서 데이터를 조회하지 못한 경우, 에러를 발생시킨다.', async () => { + (axios.get as jest.Mock).mockResolvedValue( + STOCK_INDEX_VALUE_MOCK.INVALID_DATA, + ); + + await expect( + stockIndexService.getDomesticStockIndexValueByCode('code', 'accessToken'), + ).rejects.toThrow('데이터를 정상적으로 조회하지 못했습니다.'); + }); +});