diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 36681ad9..114ecadf 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -6,11 +6,10 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { User } from './auth/user.entity'; -import { StockIndexModule } from './stock/index/stock.index.module'; -import { SocketService } from './websocket/socket.service'; -import { SocketGateway } from './websocket/socket.gateway'; -import { StockTopfiveModule } from './stock/topfive/stock.topfive.module'; -import { KoreaInvestmentModule } from './koreaInvestment/korea.investment.module'; +import { StockIndexModule } from './stock/index/stock-index.module'; +import { StockTopfiveModule } from './stock/topfive/stock-topfive.module'; +import { KoreaInvestmentModule } from './koreaInvestment/korea-investment.module'; +import { SocketModule } from './websocket/socket.module'; @Module({ imports: [ @@ -30,8 +29,9 @@ import { KoreaInvestmentModule } from './koreaInvestment/korea.investment.module AuthModule, StockIndexModule, StockTopfiveModule, + SocketModule, ], controllers: [AppController], - providers: [AppService, SocketService, SocketGateway], + providers: [AppService], }) export class AppModule {} diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 8411eb48..aee104f0 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -10,7 +10,7 @@ import { import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; import { AuthService } from './auth.service'; -import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; @Controller('auth') export class AuthController { diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index cbc4a69f..6f751e1b 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { UserRepository } from './user.repository'; -import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; @Injectable() export class AuthService { diff --git a/BE/src/auth/dto/authCredentials.dto.ts b/BE/src/auth/dto/auth-credentials.dto.ts similarity index 100% rename from BE/src/auth/dto/authCredentials.dto.ts rename to BE/src/auth/dto/auth-credentials.dto.ts diff --git a/BE/src/auth/user.repository.ts b/BE/src/auth/user.repository.ts index 0a23f980..429437e7 100644 --- a/BE/src/auth/user.repository.ts +++ b/BE/src/auth/user.repository.ts @@ -3,7 +3,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { User } from './user.entity'; -import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; @Injectable() export class UserRepository extends Repository { diff --git a/BE/src/koreaInvestment/interface/korea-investment.interface.ts b/BE/src/koreaInvestment/interface/korea-investment.interface.ts new file mode 100644 index 00000000..e290a32c --- /dev/null +++ b/BE/src/koreaInvestment/interface/korea-investment.interface.ts @@ -0,0 +1,6 @@ +export interface AccessTokenInterface { + access_token: string; + access_token_token_expired: string; + token_type: string; + expires_in: number; +} diff --git a/BE/src/koreaInvestment/korea.investment.module.ts b/BE/src/koreaInvestment/korea-investment.module.ts similarity index 76% rename from BE/src/koreaInvestment/korea.investment.module.ts rename to BE/src/koreaInvestment/korea-investment.module.ts index 69679c6b..8e7dd10a 100644 --- a/BE/src/koreaInvestment/korea.investment.module.ts +++ b/BE/src/koreaInvestment/korea-investment.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { KoreaInvestmentService } from './korea.investment.service'; +import { KoreaInvestmentService } from './korea-investment.service'; @Module({ imports: [], diff --git a/BE/src/koreaInvestment/korea.investment.service.ts b/BE/src/koreaInvestment/korea-investment.service.ts similarity index 53% rename from BE/src/koreaInvestment/korea.investment.service.ts rename to BE/src/koreaInvestment/korea-investment.service.ts index 57458372..29dc323c 100644 --- a/BE/src/koreaInvestment/korea.investment.service.ts +++ b/BE/src/koreaInvestment/korea-investment.service.ts @@ -1,4 +1,7 @@ import axios from 'axios'; +import { UnauthorizedException } from '@nestjs/common'; +import { getFullURL } from '../util/get-full-URL'; +import { AccessTokenInterface } from './interface/korea-investment.interface'; export class KoreaInvestmentService { private accessToken: string; @@ -9,19 +12,20 @@ export class KoreaInvestmentService { if (this.accessToken && this.tokenExpireTime > new Date()) { return this.accessToken; } - const response = await axios.post( - `${process.env.KOREA_INVESTMENT_BASE_URL}/oauth2/tokenP`, - { + const response = await axios + .post(getFullURL('/oauth2/tokenP'), { grant_type: 'client_credentials', appkey: process.env.KOREA_INVESTMENT_APP_KEY, appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, - }, - ); + }) + .catch(() => { + throw new UnauthorizedException('액세스 토큰을 조회하지 못했습니다.'); + }); const { data } = response; this.accessToken = data.access_token; - this.tokenExpireTime = new Date(Date.now() + +data.expires_in); + this.tokenExpireTime = new Date(data.access_token_token_expired); return this.accessToken; } diff --git a/BE/src/stock/enum/MarketType.ts b/BE/src/stock/enum/market-type.ts similarity index 100% rename from BE/src/stock/enum/MarketType.ts rename to BE/src/stock/enum/market-type.ts diff --git a/BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts b/BE/src/stock/index/dto/stock-index-list-chart.element.dto.ts similarity index 64% rename from BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts rename to BE/src/stock/index/dto/stock-index-list-chart.element.dto.ts index 1f2abdce..ba6ce1f2 100644 --- a/BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts +++ b/BE/src/stock/index/dto/stock-index-list-chart.element.dto.ts @@ -1,9 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; export class StockIndexListChartElementDto { - constructor(time: string, value: string) { + constructor(time: string, value: string, diff: string) { this.time = time; this.value = value; + this.diff = diff; } @ApiProperty({ description: 'HHMMSS', example: '130500' }) @@ -11,4 +12,7 @@ export class StockIndexListChartElementDto { @ApiProperty({ description: '주가 지수' }) value: string; + + @ApiProperty({ description: '전일 대비 주가 지수' }) + diff: string; } diff --git a/BE/src/stock/index/dto/stock-index-response-element.dto.ts b/BE/src/stock/index/dto/stock-index-response-element.dto.ts new file mode 100644 index 00000000..79cf6af5 --- /dev/null +++ b/BE/src/stock/index/dto/stock-index-response-element.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StockIndexValueElementDto } from './stock-index-value-element.dto'; +import { StockIndexListChartElementDto } from './stock-index-list-chart.element.dto'; + +export class StockIndexResponseElementDto { + @ApiProperty({ description: '실시간 값', type: StockIndexValueElementDto }) + value: StockIndexValueElementDto; + + @ApiProperty({ + description: '실시간 차트', + type: [StockIndexListChartElementDto], + }) + chart: StockIndexListChartElementDto[]; +} diff --git a/BE/src/stock/index/dto/stock-index-response.dto.ts b/BE/src/stock/index/dto/stock-index-response.dto.ts new file mode 100644 index 00000000..026fecf3 --- /dev/null +++ b/BE/src/stock/index/dto/stock-index-response.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StockIndexResponseElementDto } from './stock-index-response-element.dto'; + +export class StockIndexResponseDto { + @ApiProperty({ + description: '코스피 지수', + type: StockIndexResponseElementDto, + }) + KOSPI: StockIndexResponseElementDto; + + @ApiProperty({ + description: '코스닥 지수', + type: StockIndexResponseElementDto, + }) + KOSDAQ: StockIndexResponseElementDto; + + @ApiProperty({ + description: '코스피200 지수', + type: StockIndexResponseElementDto, + }) + KOSPI200: StockIndexResponseElementDto; + + @ApiProperty({ + description: 'KSQ150 지수', + type: StockIndexResponseElementDto, + }) + KSQ150: StockIndexResponseElementDto; +} diff --git a/BE/src/stock/index/dto/stock.index.value.element.dto.ts b/BE/src/stock/index/dto/stock-index-value-element.dto.ts similarity index 54% rename from BE/src/stock/index/dto/stock.index.value.element.dto.ts rename to BE/src/stock/index/dto/stock-index-value-element.dto.ts index 88ca4949..1eb73a50 100644 --- a/BE/src/stock/index/dto/stock.index.value.element.dto.ts +++ b/BE/src/stock/index/dto/stock-index-value-element.dto.ts @@ -1,33 +1,21 @@ import { ApiProperty } from '@nestjs/swagger'; export class StockIndexValueElementDto { - constructor( - code: string, - value: string, - diff: string, - diffRate: string, - sign: string, - ) { - this.code = code; - this.value = value; + constructor(value: string, diff: string, diffRate: string, sign: string) { + this.curr_value = value; this.diff = diff; - this.diffRate = diffRate; + this.diff_rate = diffRate; this.sign = sign; } - @ApiProperty({ - description: '코스피: 0001, 코스닥: 1001, 코스피200: 2001, KSQ150: 3003', - }) - code: string; - @ApiProperty({ description: '주가 지수' }) - value: string; + curr_value: string; @ApiProperty({ description: '전일 대비 등락' }) diff: string; @ApiProperty({ description: '전일 대비 등락률' }) - diffRate: string; + diff_rate: string; @ApiProperty({ description: '부호... 인데 추후에 알아봐야 함' }) sign: string; diff --git a/BE/src/stock/index/dto/stock.index.list.element.dto.ts b/BE/src/stock/index/dto/stock.index.list.element.dto.ts deleted file mode 100644 index 03ec0176..00000000 --- a/BE/src/stock/index/dto/stock.index.list.element.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { StockIndexListChartElementDto } from './stock.index.list.chart.element.dto'; - -export class StockIndexListElementDto { - constructor(code: string, chart: StockIndexListChartElementDto[]) { - this.code = code; - this.chart = chart; - } - - @ApiProperty({ - description: '코스피: 0001, 코스닥: 1001, 코스피200: 2001, KSQ150: 3003', - }) - code: string; - - @ApiProperty({ type: [StockIndexListChartElementDto] }) - chart: StockIndexListChartElementDto[]; -} diff --git a/BE/src/stock/index/dto/stock.index.response.dto.ts b/BE/src/stock/index/dto/stock.index.response.dto.ts deleted file mode 100644 index 358266a5..00000000 --- a/BE/src/stock/index/dto/stock.index.response.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { StockIndexListElementDto } from './stock.index.list.element.dto'; -import { StockIndexValueElementDto } from './stock.index.value.element.dto'; - -export class StockIndexResponseDto { - constructor( - indexList: StockIndexListElementDto[], - indexValue: StockIndexValueElementDto[], - ) { - this.indexList = indexList; - this.indexValue = indexValue; - } - - @ApiProperty({ - description: '주가 지수 차트 정보 (코스피, 코스닥, 코스피200, KSQ150)', - type: [StockIndexListElementDto], - }) - indexList: StockIndexListElementDto[]; - - @ApiProperty({ - description: '주가 지수 실시간 값 정보 (코스피, 코스닥, 코스피200, KSQ150)', - type: [StockIndexValueElementDto], - }) - indexValue: StockIndexValueElementDto[]; -} diff --git a/BE/src/stock/index/interface/stock.index.interface.ts b/BE/src/stock/index/interface/stock-index.interface.ts similarity index 91% rename from BE/src/stock/index/interface/stock.index.interface.ts rename to BE/src/stock/index/interface/stock-index.interface.ts index 48d5851b..93636c51 100644 --- a/BE/src/stock/index/interface/stock.index.interface.ts +++ b/BE/src/stock/index/interface/stock-index.interface.ts @@ -1,10 +1,3 @@ -export interface AccessTokenInterface { - access_token: string; - access_token_token_expired: string; - token_type: string; - expires_in: number; -} - export interface StockIndexChartInterface { output: StockIndexChartElementInterface[]; rt_cd: string; diff --git a/BE/src/stock/index/stock-index.controller.ts b/BE/src/stock/index/stock-index.controller.ts new file mode 100644 index 00000000..330685e2 --- /dev/null +++ b/BE/src/stock/index/stock-index.controller.ts @@ -0,0 +1,121 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Cron } from '@nestjs/schedule'; +import { StockIndexService } from './stock-index.service'; +import { StockIndexResponseDto } from './dto/stock-index-response.dto'; +import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; +import { SocketGateway } from '../../websocket/socket.gateway'; + +@Controller('/api/stocks/index') +@ApiTags('주가 지수 API') +export class StockIndexController { + constructor( + private readonly stockIndexService: StockIndexService, + private readonly koreaInvestmentService: KoreaInvestmentService, + private readonly socketGateway: SocketGateway, + ) {} + + @Get() + @ApiOperation({ + summary: '주가 지수 차트 정보, 현재 값 조회 API', + description: '주가 지수 차트 정보와 현재 값을 리스트로 반환한다.', + }) + @ApiResponse({ + status: 200, + description: '주가 지수 조회 성공', + type: StockIndexResponseDto, + }) + async getStockIndex() { + const accessToken = await this.koreaInvestmentService.getAccessToken(); + + const [kospiChart, kosdaqChart, kospi200Chart, ksq150Chart] = + await Promise.all([ + this.stockIndexService.getDomesticStockIndexListByCode( + '0001', + accessToken, + ), // 코스피 + this.stockIndexService.getDomesticStockIndexListByCode( + '1001', + accessToken, + ), // 코스닥 + this.stockIndexService.getDomesticStockIndexListByCode( + '2001', + accessToken, + ), // 코스피200 + this.stockIndexService.getDomesticStockIndexListByCode( + '3003', + accessToken, + ), // KSQ150 + ]); + + const [kospiValue, kosdaqValue, kospi200Value, ksq150Value] = + await Promise.all([ + this.stockIndexService.getDomesticStockIndexValueByCode( + '0001', + accessToken, + ), // 코스피 + this.stockIndexService.getDomesticStockIndexValueByCode( + '1001', + accessToken, + ), // 코스닥 + this.stockIndexService.getDomesticStockIndexValueByCode( + '2001', + accessToken, + ), // 코스피200 + this.stockIndexService.getDomesticStockIndexValueByCode( + '3003', + accessToken, + ), // KSQ150 + ]); + + const stockIndexResponse = new StockIndexResponseDto(); + stockIndexResponse.KOSPI = { + value: kospiValue, + chart: kospiChart, + }; + stockIndexResponse.KOSDAQ = { + value: kosdaqValue, + chart: kosdaqChart, + }; + stockIndexResponse.KOSPI200 = { + value: kospi200Value, + chart: kospi200Chart, + }; + stockIndexResponse.KSQ150 = { + value: ksq150Value, + chart: ksq150Chart, + }; + return stockIndexResponse; + } + + @Cron('*/5 9-16 * * 1-5') + async cronStockIndexLists() { + const accessToken = await this.koreaInvestmentService.getAccessToken(); + + const stockLists = await Promise.all([ + this.stockIndexService.getDomesticStockIndexListByCode( + '0001', + accessToken, + ), // 코스피 + this.stockIndexService.getDomesticStockIndexListByCode( + '1001', + accessToken, + ), // 코스닥 + this.stockIndexService.getDomesticStockIndexListByCode( + '2001', + accessToken, + ), // 코스피200 + this.stockIndexService.getDomesticStockIndexListByCode( + '3003', + accessToken, + ), // KSQ150 + ]); + + this.socketGateway.sendStockIndexListToClient({ + KOSPI: stockLists[0], + KOSDAQ: stockLists[1], + KOSPI200: stockLists[2], + KSQ150: stockLists[3], + }); + } +} diff --git a/BE/src/stock/index/stock.index.module.ts b/BE/src/stock/index/stock-index.module.ts similarity index 50% rename from BE/src/stock/index/stock.index.module.ts rename to BE/src/stock/index/stock-index.module.ts index 2a044bf5..64e0c575 100644 --- a/BE/src/stock/index/stock.index.module.ts +++ b/BE/src/stock/index/stock-index.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; -import { StockIndexController } from './stock.index.controller'; -import { StockIndexService } from './stock.index.service'; -import { KoreaInvestmentModule } from '../../koreaInvestment/korea.investment.module'; +import { StockIndexController } from './stock-index.controller'; +import { StockIndexService } from './stock-index.service'; +import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; +import { SocketModule } from '../../websocket/socket.module'; @Module({ - imports: [KoreaInvestmentModule], + imports: [KoreaInvestmentModule, SocketModule], controllers: [StockIndexController], providers: [StockIndexService], exports: [StockIndexService], diff --git a/BE/src/stock/index/stock-index.service.ts b/BE/src/stock/index/stock-index.service.ts new file mode 100644 index 00000000..fa05ed4c --- /dev/null +++ b/BE/src/stock/index/stock-index.service.ts @@ -0,0 +1,95 @@ +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import axios from 'axios'; +import { StockIndexListChartElementDto } from './dto/stock-index-list-chart.element.dto'; +import { StockIndexValueElementDto } from './dto/stock-index-value-element.dto'; +import { + StockIndexChartInterface, + StockIndexValueInterface, +} from './interface/stock-index.interface'; +import { getFullURL } from '../../util/get-full-URL'; +import { getHeader } from '../../util/get-header'; + +@Injectable() +export class StockIndexService { + async getDomesticStockIndexListByCode(code: string, accessToken: string) { + const result = await this.requestDomesticStockIndexListApi( + code, + accessToken, + ); + + return result.output.map((element) => { + return new StockIndexListChartElementDto( + element.bsop_hour, + element.bstp_nmix_prpr, + element.bstp_nmix_prdy_vrss, + ); + }); + } + + async getDomesticStockIndexValueByCode(code: string, accessToken: string) { + const result = await this.requestDomesticStockIndexValueApi( + code, + accessToken, + ); + + const data = result.output; + + return new StockIndexValueElementDto( + data.bstp_nmix_prpr, + data.bstp_nmix_prdy_vrss, + data.bstp_nmix_prdy_ctrt, + data.prdy_vrss_sign, + ); + } + + private async requestDomesticStockIndexListApi( + code: string, + accessToken: string, + ) { + const response = await axios + .get( + getFullURL( + '/uapi/domestic-stock/v1/quotations/inquire-index-timeprice', + ), + { + headers: getHeader(accessToken, 'FHPUP02110200'), + params: { + fid_input_hour_1: 300, + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }, + }, + ) + .catch(() => { + throw new InternalServerErrorException( + '주가 지수 차트 정보를 조회하지 못했습니다.', + ); + }); + + return response.data; + } + + private async requestDomesticStockIndexValueApi( + code: string, + accessToken: string, + ) { + const response = await axios + .get( + getFullURL('/uapi/domestic-stock/v1/quotations/inquire-index-price'), + { + headers: getHeader(accessToken, 'FHPUP02100000'), + params: { + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }, + }, + ) + .catch(() => { + throw new InternalServerErrorException( + '주가 지수 값 정보를 조회하지 못했습니다.', + ); + }); + + return response.data; + } +} diff --git a/BE/src/stock/index/stock.index.controller.ts b/BE/src/stock/index/stock.index.controller.ts deleted file mode 100644 index acc139b6..00000000 --- a/BE/src/stock/index/stock.index.controller.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { StockIndexService } from './stock.index.service'; -import { StockIndexResponseDto } from './dto/stock.index.response.dto'; -import { KoreaInvestmentService } from '../../koreaInvestment/korea.investment.service'; - -@Controller('/api/stocks/index') -@ApiTags('주가 지수 API') -export class StockIndexController { - constructor( - private readonly stockIndexService: StockIndexService, - private readonly koreaInvestmentService: KoreaInvestmentService, - ) {} - - @Get() - @ApiOperation({ - summary: '주가 지수 차트 정보, 현재 값 조회 API', - description: '주가 지수 차트 정보와 현재 값을 리스트로 반환한다.', - }) - @ApiResponse({ - status: 200, - description: '주가 지수 조회 성공', - type: StockIndexResponseDto, - }) - async getStockIndex() { - const accessToken = await this.koreaInvestmentService.getAccessToken(); - - const stockLists = await Promise.all([ - this.stockIndexService.getDomesticStockIndexListByCode( - '0001', - accessToken, - ), // 코스피 - this.stockIndexService.getDomesticStockIndexListByCode( - '1001', - accessToken, - ), // 코스닥 - this.stockIndexService.getDomesticStockIndexListByCode( - '2001', - accessToken, - ), // 코스피200 - this.stockIndexService.getDomesticStockIndexListByCode( - '3003', - accessToken, - ), // KSQ150 - ]); - - const stockValues = await Promise.all([ - this.stockIndexService.getDomesticStockIndexValueByCode( - '0001', - accessToken, - ), // 코스피 - this.stockIndexService.getDomesticStockIndexValueByCode( - '1001', - accessToken, - ), // 코스닥 - this.stockIndexService.getDomesticStockIndexValueByCode( - '2001', - accessToken, - ), // 코스피200 - this.stockIndexService.getDomesticStockIndexValueByCode( - '3003', - accessToken, - ), // KSQ150 - ]); - - return new StockIndexResponseDto(stockLists, stockValues); - } -} diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock.index.service.ts deleted file mode 100644 index f466a461..00000000 --- a/BE/src/stock/index/stock.index.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Injectable } from '@nestjs/common'; -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'; -import { - StockIndexChartInterface, - StockIndexValueInterface, -} from './interface/stock.index.interface'; - -@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: StockIndexChartInterface = await response.json(); - if (result.rt_cd !== '0') throw new Error('유효하지 않은 토큰'); - - return new StockIndexListElementDto( - code, - result.output.map((element) => { - return new StockIndexListChartElementDto( - element.bsop_hour, - element.bstp_nmix_prpr, - ); - }), - ); - } - - 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 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', - }, - }); - - 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.prdy_vrss_sign, - ); - } -} diff --git a/BE/src/stock/topfive/dto/stock-ranking-request.dto.ts b/BE/src/stock/topfive/dto/stock-ranking-request.dto.ts index 200c244d..2e9fc946 100644 --- a/BE/src/stock/topfive/dto/stock-ranking-request.dto.ts +++ b/BE/src/stock/topfive/dto/stock-ranking-request.dto.ts @@ -1,7 +1,7 @@ /** * 등락률 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO */ -export class StockRankigRequestDto { +export class StockRankingQueryParameterDto { /** * 조건 시장 분류 코드 * 'J' 주식 diff --git a/BE/src/stock/topfive/interface/stock.topfive.interface.ts b/BE/src/stock/topfive/interface/stock-topfive.interface.ts similarity index 100% rename from BE/src/stock/topfive/interface/stock.topfive.interface.ts rename to BE/src/stock/topfive/interface/stock-topfive.interface.ts diff --git a/BE/src/stock/topfive/stock.topfive.controller.ts b/BE/src/stock/topfive/stock-topfive.controller.ts similarity index 88% rename from BE/src/stock/topfive/stock.topfive.controller.ts rename to BE/src/stock/topfive/stock-topfive.controller.ts index f90d300a..bac275e3 100644 --- a/BE/src/stock/topfive/stock.topfive.controller.ts +++ b/BE/src/stock/topfive/stock-topfive.controller.ts @@ -1,8 +1,8 @@ import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; import { Controller, Get, Query } from '@nestjs/common'; -import { StockTopfiveService } from './stock.topfive.service'; +import { StockTopfiveService } from './stock-topfive.service'; import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; -import { MarketType } from '../enum/MarketType'; +import { MarketType } from '../enum/market-type'; @Controller('/api/stocks') export class StockTopfiveController { diff --git a/BE/src/stock/topfive/stock.topfive.module.ts b/BE/src/stock/topfive/stock-topfive.module.ts similarity index 68% rename from BE/src/stock/topfive/stock.topfive.module.ts rename to BE/src/stock/topfive/stock-topfive.module.ts index 33a63aae..5be62a4f 100644 --- a/BE/src/stock/topfive/stock.topfive.module.ts +++ b/BE/src/stock/topfive/stock-topfive.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { StockTopfiveController } from './stock.topfive.controller'; -import { StockTopfiveService } from './stock.topfive.service'; -import { KoreaInvestmentModule } from '../../koreaInvestment/korea.investment.module'; +import { StockTopfiveController } from './stock-topfive.controller'; +import { StockTopfiveService } from './stock-topfive.service'; +import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; @Module({ imports: [ConfigModule, KoreaInvestmentModule], diff --git a/BE/src/stock/topfive/stock-topfive.service.ts b/BE/src/stock/topfive/stock-topfive.service.ts new file mode 100644 index 00000000..82f659d5 --- /dev/null +++ b/BE/src/stock/topfive/stock-topfive.service.ts @@ -0,0 +1,156 @@ +import axios from 'axios'; +import { Injectable, Logger } from '@nestjs/common'; +import { StockRankingQueryParameterDto } from './dto/stock-ranking-request.dto'; +import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; +import { StockRankingDataDto } from './dto/stock-ranking-data.dto'; +import { MarketType } from '../enum/market-type'; +import { + StockApiOutputData, + StockApiResponse, +} from './interface/stock-topfive.interface'; +import { getHeader } from '../../util/get-header'; +import { getFullURL } from '../../util/get-full-URL'; +import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; + +@Injectable() +export class StockTopfiveService { + private readonly logger = new Logger(); + + constructor( + private readonly koreaInvestmentService: KoreaInvestmentService, + ) {} + + /** + * @private 한국투자 Open API - [국내주식] 순위 분석 - 국내주식 등락률 순위 호출 함수 + * @param {StockRankingQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - 국내주식 등락률 데이터 + * + * @author uuuo3o + */ + private async requestApi(queryParams: StockRankingQueryParameterDto) { + try { + const accessToken = await this.koreaInvestmentService.getAccessToken(); + const headers = getHeader(accessToken, 'FHPST01700000'); + const url = getFullURL('/uapi/domestic-stock/v1/ranking/fluctuation'); + const params = this.getStockRankingParams(queryParams); + + const response = await axios.get(url, { + headers, + params, + }); + + return response.data; + } catch (error) { + 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; + } + } + + /** + * 국내주식 등락률 데이터 중 필요한 시장 종류 데이터를 반환하는 함수 + * @param {MarketType} marketType - 시장 종류(ALL, KOSPI, KOSDAQ, KOSPI200) + * @returns - 상승율순, 하락율순 배열 데이터가 포함된 객체 + * + * @author uuuo3o + */ + async getMarketRanking(marketType: MarketType) { + try { + const queryParams = new StockRankingQueryParameterDto(); + queryParams.fid_cond_mrkt_div_code = 'J'; + + switch (marketType) { + case MarketType.ALL: + queryParams.fid_input_iscd = '0000'; + break; + case MarketType.KOSPI: + queryParams.fid_input_iscd = '0001'; + break; + case MarketType.KOSDAQ: + queryParams.fid_input_iscd = '1001'; + break; + case MarketType.KOSPI200: + queryParams.fid_input_iscd = '2001'; + break; + default: + break; + } + + const highResponse = await this.requestApi({ + ...queryParams, + fid_rank_sort_cls_code: '0', + }); + + const lowResponse = await this.requestApi({ + ...queryParams, + 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) { + 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; + } + } + + /** + * @private API에서 받은 국내주식 등락률 데이터를 필요한 정보로 정제하는 함수 + * @param {StockApiOutputData[]} stocks - API 응답에서 받은 원시 데이터 배열 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatStockData(stocks: StockApiOutputData[]) { + 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; + stockData.prdy_vrss = stock.prdy_vrss; + stockData.prdy_vrss_sign = stock.prdy_vrss_sign; + stockData.prdy_ctrt = stock.prdy_ctrt; + + return stockData; + }); + } + + /** + * @private 주식 순위 API 요청을 위한 쿼리 파라미터 객체 생성 함수 + * @param {StockRankingQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO + * @returns - API 요청에 필요한 쿼리 파라미터 객체 + * + * @author uuuo3o + */ + private getStockRankingParams(params: StockRankingQueryParameterDto) { + return { + 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: '', + }; + } +} diff --git a/BE/src/stock/topfive/stock.topfive.service.ts b/BE/src/stock/topfive/stock.topfive.service.ts deleted file mode 100644 index 806f3459..00000000 --- a/BE/src/stock/topfive/stock.topfive.service.ts +++ /dev/null @@ -1,142 +0,0 @@ -import axios from 'axios'; -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'; -import { StockRankingDataDto } from './dto/stock-ranking-data.dto'; -import { - StockApiOutputData, - StockApiResponse, -} from './interface/stock.topfive.interface'; -import { MarketType } from '../enum/MarketType'; -import { KoreaInvestmentService } from '../../koreaInvestment/korea.investment.service'; - -@Injectable() -export class StockTopfiveService { - private readonly koreaInvestmentConfig: { - appKey: string; - appSecret: string; - baseUrl: string; - }; - - private readonly logger = new Logger(); - - constructor( - private readonly config: ConfigService, - private readonly koreaInvestmentService: KoreaInvestmentService, - ) { - 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 requestApi(params: StockRankigRequestDto) { - try { - const token = await this.koreaInvestmentService.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) { - 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) { - 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) { - 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; - } - } - - private formatStockData(stocks: StockApiOutputData[]) { - 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; - stockData.prdy_vrss = stock.prdy_vrss; - stockData.prdy_vrss_sign = stock.prdy_vrss_sign; - stockData.prdy_ctrt = stock.prdy_ctrt; - - return stockData; - }); - } -} diff --git a/BE/src/util/get-full-URL.ts b/BE/src/util/get-full-URL.ts new file mode 100644 index 00000000..da79427f --- /dev/null +++ b/BE/src/util/get-full-URL.ts @@ -0,0 +1,3 @@ +export const getFullURL = (url: string) => { + return `${process.env.KOREA_INVESTMENT_BASE_URL}${url}`; +}; diff --git a/BE/src/util/get-header.ts b/BE/src/util/get-header.ts new file mode 100644 index 00000000..1f63ee64 --- /dev/null +++ b/BE/src/util/get-header.ts @@ -0,0 +1,10 @@ +export const getHeader = (accessToken: string, trId: string) => { + return { + '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: trId, + custtype: 'P', + }; +}; 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.gateway.ts b/BE/src/websocket/socket.gateway.ts index 921d9503..e65eac69 100644 --- a/BE/src/websocket/socket.gateway.ts +++ b/BE/src/websocket/socket.gateway.ts @@ -6,11 +6,11 @@ export class SocketGateway { @WebSocketServer() private server: Server; - sendStockIndexListToClient(stockIndex) { - this.server.emit('index', stockIndex); + sendStockIndexListToClient(stockChart) { + this.server.emit('chart', stockChart); } - sendStockIndexValueToClient(stockIndexValue) { - this.server.emit('indexValue', stockIndexValue); + sendStockIndexValueToClient(event, stockIndexValue) { + this.server.emit(event, stockIndexValue); } } diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts new file mode 100644 index 00000000..4e9e2eb2 --- /dev/null +++ b/BE/src/websocket/socket.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { SocketService } from './socket.service'; +import { SocketGateway } from './socket.gateway'; + +@Module({ + imports: [], + controllers: [], + providers: [SocketService, SocketGateway], + exports: [SocketGateway], +}) +export class SocketModule {} diff --git a/BE/src/websocket/socket.service.ts b/BE/src/websocket/socket.service.ts index 0970d7a7..0bcb7b64 100644 --- a/BE/src/websocket/socket.service.ts +++ b/BE/src/websocket/socket.service.ts @@ -1,10 +1,10 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { WebSocket } from 'ws'; -import { Cron } from '@nestjs/schedule'; +import axios from 'axios'; import { SocketGateway } from './socket.gateway'; -import { StockIndexValueElementDto } from '../stock/index/dto/stock.index.value.element.dto'; -import { StockIndexService } from '../stock/index/stock.index.service'; -import { KoreaInvestmentService } from '../koreaInvestment/korea.investment.service'; +import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; +import { SocketConnectTokenInterface } from './interface/socket.interface'; +import { getFullURL } from '../util/get-full-URL'; @Injectable() export class SocketService implements OnModuleInit { @@ -13,17 +13,19 @@ export class SocketService implements OnModuleInit { H0UPCNT0: this.handleStockIndexValue.bind(this), }; - constructor( - private readonly stockIndexGateway: SocketGateway, - private readonly stockIndexService: StockIndexService, - private readonly koreaInvestmentService: KoreaInvestmentService, - ) {} + private STOCK_CODE = { + '0001': 'KOSPI', + '1001': 'KOSDAQ', + '2001': 'KOSPI200', + '3003': 'KSQ150', + }; + + constructor(private readonly socketGateway: SocketGateway) {} async onModuleInit() { const socketConnectionKey = await this.getSocketConnectionKey(); - const url = 'ws://ops.koreainvestment.com:21000'; - this.socket = new WebSocket(url); + this.socket = new WebSocket(process.env.KOREA_INVESTMENT_SOCKET_URL); this.socket.onopen = () => { this.registerStockIndexByCode('0001', socketConnectionKey); // 코스피 @@ -43,37 +45,11 @@ export class SocketService implements OnModuleInit { }; } - @Cron('*/5 9-16 * * 1-5') - async cronStockIndexLists() { - const accessToken = await this.koreaInvestmentService.getAccessToken(); - - const stockLists = await Promise.all([ - this.stockIndexService.getDomesticStockIndexListByCode( - '0001', - accessToken, - ), // 코스피 - this.stockIndexService.getDomesticStockIndexListByCode( - '1001', - accessToken, - ), // 코스닥 - this.stockIndexService.getDomesticStockIndexListByCode( - '2001', - accessToken, - ), // 코스피200 - this.stockIndexService.getDomesticStockIndexListByCode( - '3003', - accessToken, - ), // KSQ150 - ]); - - this.stockIndexGateway.sendStockIndexListToClient(stockLists); - } - private handleStockIndexValue(responseData: string) { const responseList = responseData.split('^'); - this.stockIndexGateway.sendStockIndexValueToClient( + this.socketGateway.sendStockIndexValueToClient( + this.STOCK_CODE[responseList[0]], new StockIndexValueElementDto( - responseList[0], responseList[2], responseList[4], responseList[9], @@ -83,20 +59,16 @@ export class SocketService implements OnModuleInit { } private async getSocketConnectionKey() { - const url = 'https://openapi.koreainvestment.com:9443/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( + getFullURL('/oauth2/Approval'), + { grant_type: 'client_credentials', - appkey: process.env.APP_KEY, - secretkey: process.env.APP_SECRET, - }), - }); - const result: SocketConnectTokenInterface = await response.json(); + appkey: process.env.KOREA_INVESTMENT_APP_KEY, + secretkey: process.env.KOREA_INVESTMENT_APP_SECRET, + }, + ); + + const result = response.data; return result.approval_key; } @@ -119,9 +91,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..9a844581 --- /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..2008f2e4 --- /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('데이터를 정상적으로 조회하지 못했습니다.'); + }); +});