diff --git a/BE/.eslintrc.js b/BE/.eslintrc.js index 0cea1c7c..b54cc5a8 100644 --- a/BE/.eslintrc.js +++ b/BE/.eslintrc.js @@ -34,5 +34,6 @@ module.exports = { 'class-methods-use-this': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/naming-convention': 'off', }, }; diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index ac06a59c..93e45a4e 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -10,6 +10,7 @@ import { StockTopfiveModule } from './stock/topfive/stock-topfive.module'; import { KoreaInvestmentModule } from './koreaInvestment/korea-investment.module'; import { SocketModule } from './websocket/socket.module'; import { StockOrderModule } from './stock/order/stock-order.module'; +import { StockDetailModule } from './stock/detail/stock-detail.module'; import { typeOrmConfig } from './configs/typeorm.config'; @Module({ @@ -22,6 +23,7 @@ import { typeOrmConfig } from './configs/typeorm.config'; StockIndexModule, StockTopfiveModule, SocketModule, + StockDetailModule, StockOrderModule, ], controllers: [AppController], diff --git a/BE/src/stock/detail/dto/stock-detail-output1.dto.ts b/BE/src/stock/detail/dto/stock-detail-output1.dto.ts new file mode 100644 index 00000000..aa911bd1 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-output1.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class InquirePriceOutput1Dto { + @ApiProperty({ description: 'HTS 한글 종목명' }) + hts_kor_isnm: string; + + @ApiProperty({ description: '종목코드' }) + stck_shrn_iscd: string; + + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; + + @ApiProperty({ description: 'HTS 시가총액' }) + hts_avls: string; + + @ApiProperty({ description: 'PER' }) + per: string; +} diff --git a/BE/src/stock/detail/dto/stock-detail-output2.dto.ts b/BE/src/stock/detail/dto/stock-detail-output2.dto.ts new file mode 100644 index 00000000..c6df900f --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-output2.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class InquirePriceOutput2Dto { + @ApiProperty({ description: '주식 영업 일자' }) + stck_bsop_date: string; + + @ApiProperty({ description: '주식 종가' }) + stck_clpr: string; + + @ApiProperty({ description: '주식 시가' }) + stck_oprc: string; + + @ApiProperty({ description: '주식 최고가' }) + stck_hgpr: string; + + @ApiProperty({ description: '주식 최저가' }) + stck_lwpr: string; + + @ApiProperty({ description: '누적 거래량' }) + acml_vol: string; + + @ApiProperty({ description: '누적 거래 대금' }) + acml_tr_pbmn: string; + + @ApiProperty({ description: '락 구분 코드' }) + flng_cls_code: string; + + @ApiProperty({ description: '분할 비율' }) + prtt_rate: string; + + @ApiProperty({ description: '분할변경여부' }) + mod_yn: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '재평가사유코드' }) + revl_issu_reas: string; +} diff --git a/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts b/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts new file mode 100644 index 00000000..feb9ca0f --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts @@ -0,0 +1,34 @@ +/** + * 주식 현재가 시세 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO + */ +export class StockDetailQueryParameterDto { + /** + * 조건 시장 분류 코드 + * 'J' 주식 + */ + fid_cond_mrkt_div_code: string; + + /** + * 주식 종목 코드 + * (ex) 005930 + */ + fid_input_iscd: string; + + /** + * 조회 시작일자 + * (ex) 20220501 + */ + fid_input_date_1: string; + + /** + * 조회 종료일자 + * (ex) 20220530 + */ + fid_input_date_2: string; + + /** + * 기간 분류 코드 + * D:일봉, W:주봉, M:월봉, Y:년봉 + */ + fid_period_div_code: string; +} diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-request.dto.ts new file mode 100644 index 00000000..7bd1b1d8 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-request.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * 국내주식기간별시세(일/주/월/년) API를 이용할 때 필요한 요청 데이터를 담고 있는 DTO + */ +export class StockDetailRequestDto { + @ApiProperty({ description: '조회 시작일자 (ex) 20220501' }) + fid_input_date_1: string; + + @ApiProperty({ description: '조회 종료일자 (ex) 20220530' }) + fid_input_date_2: string; + + @ApiProperty({ + description: '기간 분류 코드 (ex) D(일봉) W(주봉) M(월봉) Y(년봉)', + }) + fid_period_div_code: string; +} diff --git a/BE/src/stock/detail/dto/stock-detail-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts new file mode 100644 index 00000000..a86f9e04 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-response.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { InquirePriceOutput1Dto } from './stock-detail-output1.dto'; +import { InquirePriceOutput2Dto } from './stock-detail-output2.dto'; + +/** + * 국내주식기간별시세(일/주/월/년) API 응답값 정제 후 FE에 보낼 DTO + */ +export class InquirePriceResponseDto { + @ApiProperty({ type: InquirePriceOutput1Dto, description: '상승률 순위' }) + output1: InquirePriceOutput1Dto; + + @ApiProperty({ type: [InquirePriceOutput2Dto], description: '하락률 순위' }) + output2: InquirePriceOutput2Dto[]; +} diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail.interface.ts new file mode 100644 index 00000000..1169eb90 --- /dev/null +++ b/BE/src/stock/detail/interface/stock-detail.interface.ts @@ -0,0 +1,56 @@ +export interface InquirePriceOutput1Data { + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + stck_prdy_clpr: string; + acml_vol: string; + acml_tr_pbmn: string; + hts_kor_isnm: string; + stck_prpr: string; + stck_shrn_iscd: string; + prdy_vol: string; + stck_mxpr: string; + stck_llam: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + stck_prdy_oprc: string; + stck_prdy_hgpr: string; + stck_prdy_lwpr: string; + askp: string; + bidp: string; + prdy_vrss_vol: string; + vol_tnrt: string; + stck_fcam: string; + lstn_stcn: string; + cpfn: string; + hts_avls: string; + per: string; + eps: string; + pbr: string; + itewhol_loan_rmnd_ratem_name: string; +} + +export interface InquirePriceOutput2Data { + stck_bsop_date: string; + stck_clpr: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + acml_vol: string; + acml_tr_pbmn: string; + flng_cls_code: string; + prtt_rate: string; + mod_yn: string; + prdy_vrss_sign: string; + prdy_vrss: string; + revl_issu_reas: string; +} + +export interface InquirePriceApiResponse { + output1: InquirePriceOutput1Data; + output2: InquirePriceOutput2Data[]; + rt_cd: string; + msg_cd: string; + msg1: string; +} diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts new file mode 100644 index 00000000..5c4cfa85 --- /dev/null +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -0,0 +1,45 @@ +import { Body, Controller, Param, Post } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { StockDetailService } from './stock-detail.service'; +import { StockDetailRequestDto } from './dto/stock-detail-request.dto'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; + +@Controller('/api/stocks') +export class StockDetailController { + constructor(private readonly stockDetailService: StockDetailService) {} + + @Post(':stockCode') + @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiBody({ + description: + '주식 상세 조회에 필요한 데이터\n\n' + + 'fid_input_date_1: 조회 시작일자 (ex) 20240505\n\n' + + 'fid_input_date_2: 조회 종료일자 (ex) 20241111\n\n' + + 'fid_period_div_code: 기간 분류 코드 (ex) D(일봉), W(주봉), M(월봉), Y(년봉)', + type: StockDetailRequestDto, + }) + @ApiResponse({ + status: 201, + description: '단일 주식 종목 기본값 조회 성공', + type: InquirePriceResponseDto, + }) + getStockDetail( + @Param('stockCode') stockCode: string, + @Body() body: StockDetailRequestDto, + ) { + const { fid_input_date_1, fid_input_date_2, fid_period_div_code } = body; + return this.stockDetailService.getInquirePrice( + stockCode, + fid_input_date_1, + fid_input_date_2, + fid_period_div_code, + ); + } +} diff --git a/BE/src/stock/detail/stock-detail.module.ts b/BE/src/stock/detail/stock-detail.module.ts new file mode 100644 index 00000000..cfb2b57b --- /dev/null +++ b/BE/src/stock/detail/stock-detail.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; +import { StockDetailController } from './stock-detail.controller'; +import { StockDetailService } from './stock-detail.service'; + +@Module({ + imports: [KoreaInvestmentModule], + controllers: [StockDetailController], + providers: [StockDetailService], +}) +export class StockDetailModule {} diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts new file mode 100644 index 00000000..c0629406 --- /dev/null +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -0,0 +1,144 @@ +import axios from 'axios'; +import { Injectable, Logger } from '@nestjs/common'; +import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; +import { getHeader } from '../../util/get-header'; +import { getFullURL } from '../../util/get-full-URL'; +import { InquirePriceApiResponse } from './interface/stock-detail.interface'; +import { StockDetailQueryParameterDto } from './dto/stock-detail-query-parameter.dto'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; + +@Injectable() +export class StockDetailService { + private readonly logger = new Logger(); + + constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + + /** + * 특정 주식의 기간별시세 데이터를 반환하는 함수 + * @param {string} stockCode - 종목코드 + * @param {string} date1 - 조회 시작일자 + * @param {string} date2 - 조회 종료일자 + * @param {string} periodDivCode - 기간 분류 코드 + * @returns - 특정 주식의 기간별시세 데이터 객체 반환 + * + * @author uuuo3o + */ + async getInquirePrice( + stockCode: string, + date1: string, + date2: string, + periodDivCode: string, + ) { + try { + const queryParams = new StockDetailQueryParameterDto(); + queryParams.fid_cond_mrkt_div_code = 'J'; + queryParams.fid_input_iscd = stockCode; + queryParams.fid_input_date_1 = date1; + queryParams.fid_input_date_2 = date2; + queryParams.fid_period_div_code = periodDivCode; + + const response = await this.requestApi(queryParams); + + return this.formatStockData(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 한국투자 Open API - [국내주식] 기본시세 - 국내주식기간별시세(일/주/월/년) 호출 함수 + * @param {StockDetailQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - 국내주식기간별시세(일/주/월/년) 데이터 + * + * @author uuuo3o + */ + private async requestApi(queryParams: StockDetailQueryParameterDto) { + try { + const accessToken = await this.koreaInvetmentService.getAccessToken(); + const headers = getHeader(accessToken, 'FHKST03010100'); + const url = getFullURL( + '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', + ); + const params = this.getInquirePriceParams(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; + } + } + + /** + * @private API에서 받은 국내주식기간별시세(일/주/월/년) 데이터를 필요한 정보로 정제하는 함수 + * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatStockData(response: InquirePriceApiResponse) { + const stockData = new InquirePriceResponseDto(); + const { output1, output2 } = response; + + const { + hts_kor_isnm, + stck_shrn_iscd, + stck_prpr, + prdy_vrss, + prdy_vrss_sign, + prdy_ctrt, + hts_avls, + per, + } = output1; + + stockData.output1 = { + hts_kor_isnm, + stck_shrn_iscd, + stck_prpr, + prdy_vrss, + prdy_vrss_sign, + prdy_ctrt, + hts_avls, + per, + }; + + stockData.output2 = output2; + + return stockData; + } + + /** + * @private 국내주식기간별시세(일/주/월/년) 요청을 위한 쿼리 파라미터 객체 생성 함수 + * @param {StockDetailQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO + * @returns - API 요청에 필요한 쿼리 파라미터 객체 + * + * @author uuuo3o + */ + private getInquirePriceParams(params: StockDetailQueryParameterDto) { + return { + fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, + fid_input_iscd: params.fid_input_iscd, + fid_input_date_1: params.fid_input_date_1, + fid_input_date_2: params.fid_input_date_2, + fid_period_div_code: params.fid_period_div_code, + fid_org_adj_prc: 0, + }; + } +}