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

[#6] 3.02 주식차트 정보 기능 구현 #32

Merged
merged 17 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
9,846 changes: 866 additions & 8,980 deletions BE/package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion BE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.4.7",
"@nestjs/schedule": "^4.1.1",
"@nestjs/swagger": "^8.0.1",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.4.7",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"docker": "^1.0.0",
Expand All @@ -34,8 +37,10 @@
"mysql2": "^3.11.3",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
"swagger-ui-express": "^5.0.1",
"typeorm": "^0.3.20"
"typeorm": "^0.3.20",
"ws": "^8.18.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand All @@ -45,6 +50,7 @@
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@types/ws": "^8.5.13",
"eslint": "^8.0.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^18.0.0",
Expand Down
8 changes: 7 additions & 1 deletion BE/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StockIndexModule } from './stock/index/stock.index.module';
import { SocketService } from './websocket/socket.service';
import { SocketGateway } from './websocket/socket.gateway';

@Module({
imports: [
ScheduleModule.forRoot(),
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'mysql', // 데이터베이스 타입
Expand All @@ -17,8 +22,9 @@ import { AppService } from './app.service';
entities: [],
synchronize: true,
}),
StockIndexModule,
],
controllers: [AppController],
providers: [AppService],
providers: [AppService, SocketService, SocketGateway],
})
export class AppModule {}
14 changes: 14 additions & 0 deletions BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';

export class StockIndexListChartElementDto {
constructor(time: string, value: string) {
this.time = time;
this.value = value;
}

@ApiProperty({ description: 'HHMMSS', example: '130500' })
time: string;

@ApiProperty({ description: '주가 지수' })
value: string;
}
17 changes: 17 additions & 0 deletions BE/src/stock/index/dto/stock.index.list.element.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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[];
}
25 changes: 25 additions & 0 deletions BE/src/stock/index/dto/stock.index.response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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[];
}
34 changes: 34 additions & 0 deletions BE/src/stock/index/dto/stock.index.value.element.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;
this.diff = diff;
this.diffRate = diffRate;
this.sign = sign;
}

@ApiProperty({
description: '코스피: 0001, 코스닥: 1001, 코스피200: 2001, KSQ150: 3003',
})
code: string;

@ApiProperty({ description: '주가 지수' })
value: string;

@ApiProperty({ description: '전일 대비 등락' })
diff: string;

@ApiProperty({ description: '전일 대비 등락률' })
diffRate: string;

@ApiProperty({ description: '부호... 인데 추후에 알아봐야 함' })
sign: string;
}
38 changes: 38 additions & 0 deletions BE/src/stock/index/stock.index.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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';

@Controller('/api/stock/index')
@ApiTags('주가 지수 API')
export class StockIndexController {
constructor(private readonly stockIndexService: StockIndexService) {}

@Get()
@ApiOperation({
summary: '주가 지수 차트 정보, 현재 값 조회 API',
description: '주가 지수 차트 정보와 현재 값을 리스트로 반환한다.',
})
@ApiResponse({
status: 200,
description: '주가 지수 조회 성공',
type: StockIndexResponseDto,
})
async getStockIndex() {
const stockLists = await Promise.all([
this.stockIndexService.getDomesticStockIndexListByCode('0001'), // 코스피
this.stockIndexService.getDomesticStockIndexListByCode('1001'), // 코스닥
this.stockIndexService.getDomesticStockIndexListByCode('2001'), // 코스피200
this.stockIndexService.getDomesticStockIndexListByCode('3003'), // KSQ150
]);

const stockValues = await Promise.all([
this.stockIndexService.getDomesticStockIndexValueByCode('0001'), // 코스피
this.stockIndexService.getDomesticStockIndexValueByCode('1001'), // 코스닥
this.stockIndexService.getDomesticStockIndexValueByCode('2001'), // 코스피200
this.stockIndexService.getDomesticStockIndexValueByCode('3003'), // KSQ150
]);

return new StockIndexResponseDto(stockLists, stockValues);
}
}
11 changes: 11 additions & 0 deletions BE/src/stock/index/stock.index.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { StockIndexController } from './stock.index.controller';
import { StockIndexService } from './stock.index.service';

@Module({
imports: [],
controllers: [StockIndexController],
providers: [StockIndexService],
exports: [StockIndexService],
})
export class StockIndexModule {}
168 changes: 168 additions & 0 deletions BE/src/stock/index/stock.index.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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';

@Injectable()
export class StockIndexService {
private accessToken: string;
private expireDateTime: number;

async getDomesticStockIndexListByCode(code: string) {
const accessToken = await this.getAccessToken();

const url =
'https://openapi.koreainvestment.com:9443/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.APP_KEY,
appsecret: process.env.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) {
const accessToken = await this.getAccessToken();

const url =
'https://openapi.koreainvestment.com:9443/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.APP_KEY,
appsecret: process.env.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,
);
}

private async getAccessToken() {
if (!this.accessToken || this.expireDateTime <= Date.now()) {
const url = 'https://openapivts.koreainvestment.com:29443/oauth2/tokenP';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: JSON.stringify({
grant_type: 'client_credentials',
appkey: process.env.APP_KEY,
appsecret: process.env.APP_SECRET,
}),
});
const result: AccessTokenInterface = await response.json();
this.accessToken = result.access_token;
this.expireDateTime = new Date(
result.access_token_token_expired,
).getTime();
return result.access_token;
}

return this.accessToken;
}
}

// interfaces

interface AccessTokenInterface {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 이후에 이런 interface 어디서 관리하실 건가요?
저도 지금 service 계층에 그냥 두고 사용 중인데 나중에 분리가 필요하지 않을까 싶어서 질문드립니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앜 인터페이스 아마 각 도메인에 interface 디렉터리 두고 관리할 것 같아요! 추가로 파일 분리하면 자꾸 린트 오류 떠서 규칙도 수정해야할 것 같슴니당 참고자료

access_token: string;
access_token_token_expired: string;
token_type: string;
expires_in: number;
}

interface StockIndexChartInterface {
output: StockIndexChartElementInterface[];
rt_cd: string;
msg_cd: string;
msg1: string;
}

interface StockIndexChartElementInterface {
bsop_hour: string;
bstp_nmix_prpr: string;
bstp_nmix_prdy_vrss: string;
prdy_vrss_sign: string;
bstp_nmix_prdy_ctrt: string;
acml_tr_pbmn: string;
acml_vol: string;
cntg_vol: string;
}

interface StockIndexValueInterface {
output: {
bstp_nmix_prpr: string;
bstp_nmix_prdy_vrss: string;
prdy_vrss_sign: string;
bstp_nmix_prdy_ctrt: string;
acml_vol: string;
prdy_vol: string;
acml_tr_pbmn: string;
prdy_tr_pbmn: string;
bstp_nmix_oprc: string;
prdy_nmix_vrss_nmix_oprc: string;
oprc_vrss_prpr_sign: string;
bstp_nmix_oprc_prdy_ctrt: string;
bstp_nmix_hgpr: string;
prdy_nmix_vrss_nmix_hgpr: string;
hgpr_vrss_prpr_sign: string;
bstp_nmix_hgpr_prdy_ctrt: string;
bstp_nmix_lwpr: string;
prdy_clpr_vrss_lwpr: string;
lwpr_vrss_prpr_sign: string;
prdy_clpr_vrss_lwpr_rate: string;
ascn_issu_cnt: string;
uplm_issu_cnt: string;
stnr_issu_cnt: string;
down_issu_cnt: string;
lslm_issu_cnt: string;
dryy_bstp_nmix_hgpr: string;
dryy_hgpr_vrss_prpr_rate: string;
dryy_bstp_nmix_hgpr_date: string;
dryy_bstp_nmix_lwpr: string;
dryy_lwpr_vrss_prpr_rate: string;
dryy_bstp_nmix_lwpr_date: string;
total_askp_rsqn: string;
total_bidp_rsqn: string;
seln_rsqn_rate: string;
shnu_rsqn_rate: string;
ntby_rsqn: string;
};
rt_cd: string;
msg_cd: string;
msg1: string;
}
16 changes: 16 additions & 0 deletions BE/src/websocket/socket.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway({ namespace: 'socket', cors: { origin: '*' } })
export class SocketGateway {
@WebSocketServer()
private server: Server;

sendStockIndexListToClient(stockIndex) {
this.server.emit('index', stockIndex);
}

sendStockIndexValueToClient(stockIndexValue) {
this.server.emit('indexValue', stockIndexValue);
}
}
Loading