-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
livedata 수집 추가 + openapi로 장시간,마감 변경 로직, token을 주입할 수 있게 변경 (#246)
* ✨ feat: token entity 추가 * ✨ feat: entity 에 저장, expire 검사 로직 추가 * 🐛 fix: token 주입으로 로직 변경 * ♻️ refactor: token 주입으로 변경, 그로 인한 오류 수정 및 console.log 삭제 * 🐛 fix: live 데이터 수집 오류 해결, 데이터 없을 때 insert 오류 해결 * 🐛 fix: stock, livedata entity 수정 * ♻️ refactor: develop 환경시 logging 활성화 * 📦️ ci: production 환경일 때 작동되게 변경 * ♻️ refactor: websocket 모듈에서 liveData로 서비스 로직 분리 * ♻️ refactor: livedata stock module로 이동 * ✨ feat: 장 마감시 openapi로 부르는 로직 추가 * 🐛 fix: websocket logger 추가, client stock 저장되지 않는 오류 해결 * 🐛 fix: openapi는 저장이 필요 없음 롤 백 * 🐛 fix: open api로 데이터 받지 못하는 문제 해결 * 💄 style: 안 쓰이는 것 빼기 * 💄 style: console.log 삭제
- Loading branch information
1 parent
46a9b28
commit eafa2b3
Showing
15 changed files
with
425 additions
and
182 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
packages/backend/src/scraper/openapi/liveData.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import { Cron } from '@nestjs/schedule'; | ||
import { Logger } from 'winston'; | ||
import { RawData, WebSocket } from 'ws'; | ||
import { OpenapiLiveData } from './api/openapiLiveData.api'; | ||
import { OpenapiTokenApi } from './api/openapiToken.api'; | ||
import { openApiConfig } from './config/openapi.config'; | ||
import { parseMessage } from './parse/openapi.parser'; | ||
import { WebsocketClient } from './websocket/websocketClient.websocket'; | ||
|
||
type TR_IDS = '0' | '1'; | ||
|
||
@Injectable() | ||
export class LiveData { | ||
private readonly clientStock: Set<string> = new Set(); | ||
private readonly reconnectInterval = 60 * 1000 * 1000; | ||
|
||
private readonly startTime: Date = new Date(2024, 0, 1, 9, 0, 0, 0); | ||
private readonly endTime: Date = new Date(2024, 0, 1, 15, 30, 0, 0); | ||
constructor( | ||
private readonly openApiToken: OpenapiTokenApi, | ||
private readonly webSocketClient: WebsocketClient, | ||
private readonly openapiLiveData: OpenapiLiveData, | ||
@Inject('winston') private readonly logger: Logger, | ||
) { | ||
this.connect(); | ||
this.subscribe('000020'); | ||
} | ||
|
||
private async openapiSubscribe(stockId: string) { | ||
const config = (await this.openApiToken.configs())[0]; | ||
const result = await this.openapiLiveData.connectLiveData(stockId, config); | ||
this.logger.info(JSON.stringify(result)); | ||
try { | ||
const stockLiveData = this.openapiLiveData.convertResponseToStockLiveData( | ||
result.output, | ||
stockId, | ||
); | ||
if (stockLiveData) { | ||
this.openapiLiveData.saveLiveData([stockLiveData]); | ||
} | ||
} catch (error) { | ||
this.logger.warn(`Subscribe error in open api : ${error}`); | ||
} | ||
} | ||
|
||
async subscribe(stockId: string) { | ||
if (this.isCloseTime(new Date(), this.startTime, this.endTime)) { | ||
await this.openapiSubscribe(stockId); | ||
} else { | ||
// TODO : 하나의 config만 사용중. | ||
this.clientStock.add(stockId); | ||
const message = this.convertObjectToMessage( | ||
(await this.openApiToken.configs())[0], | ||
stockId, | ||
'1', | ||
); | ||
this.webSocketClient.subscribe(message); | ||
} | ||
} | ||
|
||
async discribe(stockId: string) { | ||
if (this.clientStock.has(stockId)) { | ||
this.clientStock.delete(stockId); | ||
const message = this.convertObjectToMessage( | ||
(await this.openApiToken.configs())[0], | ||
stockId, | ||
'0', | ||
); | ||
this.webSocketClient.discribe(message); | ||
} | ||
} | ||
|
||
private initOpenCallback = | ||
(sendMessage: (message: string) => void) => async () => { | ||
this.logger.info('WebSocket connection established'); | ||
for (const stockId of this.clientStock.keys()) { | ||
const message = this.convertObjectToMessage( | ||
(await this.openApiToken.configs())[0], | ||
stockId, | ||
'1', | ||
); | ||
sendMessage(message); | ||
} | ||
}; | ||
|
||
private initMessageCallback = | ||
(client: WebSocket) => async (data: RawData) => { | ||
try { | ||
const message = this.parseMessage(data); | ||
if (message.header) { | ||
if (message.header.tr_id === 'PINGPONG') { | ||
this.logger.info(`Received PING: ${data}`); | ||
client.pong(data); | ||
} | ||
return; | ||
} | ||
this.logger.info(`Recived data : ${data}`); | ||
this.logger.info(`Stock id : ${message[0]['STOCK_ID']}`); | ||
const liveData = this.openapiLiveData.convertLiveData(message); | ||
await this.openapiLiveData.saveLiveData(liveData); | ||
} catch (error) { | ||
this.logger.warn(error); | ||
} | ||
}; | ||
|
||
private initCloseCallback = () => { | ||
this.logger.warn( | ||
`WebSocket connection closed. Reconnecting in ${this.reconnectInterval / 60 / 1000} minute...`, | ||
); | ||
}; | ||
|
||
private initErrorCallback = (error: unknown) => { | ||
if (error instanceof Error) { | ||
this.logger.error(`WebSocket error: ${error.message}`); | ||
} else { | ||
this.logger.error('WebSocket error: callback function'); | ||
} | ||
setTimeout(() => this.connect(), this.reconnectInterval); | ||
}; | ||
|
||
private isCloseTime(date: Date, start: Date, end: Date): boolean { | ||
const dateMinutes = date.getHours() * 60 + date.getMinutes(); | ||
const startMinutes = start.getHours() * 60 + start.getMinutes(); | ||
const endMinutes = end.getHours() * 60 + end.getMinutes(); | ||
|
||
return dateMinutes <= startMinutes || dateMinutes >= endMinutes; | ||
} | ||
|
||
@Cron('0 2 * * 1-5') | ||
connect() { | ||
this.webSocketClient.connectPacade( | ||
this.initOpenCallback, | ||
this.initMessageCallback, | ||
this.initCloseCallback, | ||
this.initErrorCallback, | ||
); | ||
} | ||
|
||
private convertObjectToMessage( | ||
config: typeof openApiConfig, | ||
stockId: string, | ||
tr_type: TR_IDS, | ||
): string { | ||
this.logger.info(JSON.stringify(config)); | ||
const message = { | ||
header: { | ||
approval_key: config.STOCK_WEBSOCKET_KEY!, | ||
custtype: 'P', | ||
tr_type, | ||
'content-type': 'utf-8', | ||
}, | ||
body: { | ||
input: { | ||
tr_id: 'H0STCNT0', | ||
tr_key: stockId, | ||
}, | ||
}, | ||
}; | ||
return JSON.stringify(message); | ||
} | ||
|
||
private parseMessage(data: RawData) { | ||
if (typeof data === 'object' && !(data instanceof Buffer)) { | ||
return data; | ||
} else if (typeof data === 'object') { | ||
return parseMessage(data.toString()); | ||
} else { | ||
return parseMessage(data as string); | ||
} | ||
} | ||
} |
Oops, something went wrong.