From feca84cf29dd14295a6608af4e0c064277665f4f Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 13:54:59 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20stockSocke?= =?UTF-8?q?t=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=EC=97=90=20stock-index?= =?UTF-8?q?-socket.service,=20stock-price-socket.service=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=B4=EC=84=9C=20=EA=B4=80=EB=A6=AC=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base-stock-socket.domain-service.ts | 24 +++ .../stock-execute-order.repository.ts | 105 +++++++++++++ .../stockSocket/stock-index-socket.service.ts | 41 +++++ .../stockSocket/stock-price-socket.service.ts | 145 ++++++++++++++++++ BE/src/stockSocket/stock-socket.module.ts | 16 ++ 5 files changed, 331 insertions(+) create mode 100644 BE/src/stockSocket/base-stock-socket.domain-service.ts create mode 100644 BE/src/stockSocket/stock-execute-order.repository.ts create mode 100644 BE/src/stockSocket/stock-index-socket.service.ts create mode 100644 BE/src/stockSocket/stock-price-socket.service.ts create mode 100644 BE/src/stockSocket/stock-socket.module.ts diff --git a/BE/src/stockSocket/base-stock-socket.domain-service.ts b/BE/src/stockSocket/base-stock-socket.domain-service.ts new file mode 100644 index 00000000..6bd5b571 --- /dev/null +++ b/BE/src/stockSocket/base-stock-socket.domain-service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { SocketGateway } from '../common/websocket/socket.gateway'; +import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; + +@Injectable() +export abstract class BaseStockSocketDomainService { + protected constructor( + protected readonly socketGateway: SocketGateway, + protected readonly baseSocketDomainService: BaseSocketDomainService, + protected readonly TR_ID: string, + ) { + baseSocketDomainService.registerSocketOpenHandler(() => + this.socketOpenHandler(), + ); + + baseSocketDomainService.registerSocketDataHandler(TR_ID, (data: string[]) => + this.socketDataHandler(data), + ); + } + + abstract socketOpenHandler(): void | Promise; + + abstract socketDataHandler(data: string[]): void; +} diff --git a/BE/src/stockSocket/stock-execute-order.repository.ts b/BE/src/stockSocket/stock-execute-order.repository.ts new file mode 100644 index 00000000..3a0eca92 --- /dev/null +++ b/BE/src/stockSocket/stock-execute-order.repository.ts @@ -0,0 +1,105 @@ +import { DataSource, Repository } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { InternalServerErrorException } from '@nestjs/common'; +import { Order } from '../stock/order/stock-order.entity'; +import { StatusType } from '../stock/order/enum/status-type'; +import { Asset } from '../asset/asset.entity'; +import { UserStock } from '../asset/user-stock.entity'; + +export class StockExecuteOrderRepository extends Repository { + constructor(@InjectDataSource() private dataSource: DataSource) { + super(Order, dataSource.createEntityManager()); + } + + async findAllCodeByStatus() { + return this.createQueryBuilder('orders') + .select('DISTINCT orders.stock_code') + .where({ status: StatusType.PENDING }) + .getRawMany(); + } + + async updateOrderAndAssetAndUserStockWhenBuy(order, realPrice) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => `cash_balance - :realPrice`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameters({ realPrice }) + .execute(); + + await queryRunner.query( + `INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE avg_price = (avg_price * quantity + ? * ?) / (quantity + ?), quantity = quantity + ?`, + [ + order.user_id, + order.stock_code, + order.amount, + order.price, + new Date(), + order.price, + order.amount, + order.amount, + order.amount, + ], + ); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(err); + } finally { + await queryRunner.release(); + } + } + + async updateOrderAndAssetAndUserStockWhenSell(order, realPrice) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => `cash_balance + :realPrice`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameters({ realPrice }) + .execute(); + + await queryRunner.manager + .createQueryBuilder() + .update(UserStock) + .set({ + quantity: () => `quantity - :newQuantity`, + }) + .where({ user_id: order.user_id, stock_code: order.stock_code }) + .setParameters({ newQuantity: order.amount }) + .execute(); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } + } +} diff --git a/BE/src/stockSocket/stock-index-socket.service.ts b/BE/src/stockSocket/stock-index-socket.service.ts new file mode 100644 index 00000000..208fc71f --- /dev/null +++ b/BE/src/stockSocket/stock-index-socket.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; +import { BaseStockSocketDomainService } from './base-stock-socket.domain-service'; +import { SocketGateway } from '../common/websocket/socket.gateway'; +import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; + +@Injectable() +export class StockIndexSocketService extends BaseStockSocketDomainService { + private STOCK_CODE = { + '0001': 'KOSPI', + '1001': 'KOSDAQ', + '2001': 'KOSPI200', + '3003': 'KSQ150', + }; + + constructor( + protected readonly socketGateway: SocketGateway, + protected readonly baseSocketDomainService: BaseSocketDomainService, + ) { + super(socketGateway, baseSocketDomainService, 'H0UPCNT0'); + } + + socketOpenHandler(): void { + this.baseSocketDomainService.registerCode(this.TR_ID, '0001'); // 코스피 + this.baseSocketDomainService.registerCode(this.TR_ID, '1001'); // 코스닥 + this.baseSocketDomainService.registerCode(this.TR_ID, '2001'); // 코스피200 + this.baseSocketDomainService.registerCode(this.TR_ID, '3003'); // KSQ150 + } + + socketDataHandler(data: string[]): void { + this.socketGateway.sendStockIndexValueToClient( + this.STOCK_CODE[data[0]], + new StockIndexValueElementDto( + data[2], // 주가 지수 + data[4], // 전일 대비 등락 + data[9], // 전일 대비 등락률 + data[3], // 부호 + ), + ); + } +} diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts new file mode 100644 index 00000000..60695e13 --- /dev/null +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -0,0 +1,145 @@ +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; +import { SocketGateway } from '../common/websocket/socket.gateway'; +import { BaseStockSocketDomainService } from './base-stock-socket.domain-service'; +import { Order } from '../stock/order/stock-order.entity'; +import { TradeType } from '../stock/order/enum/trade-type'; +import { StatusType } from '../stock/order/enum/status-type'; +import { TodayStockTradeHistoryDataDto } from '../stock/trade/history/dto/today-stock-trade-history-data.dto'; +import { StockDetailSocketDataDto } from '../stock/trade/history/dto/stock-detail-socket-data.dto'; +import { StockExecuteOrderRepository } from './stock-execute-order.repository'; + +@Injectable() +export class StockPriceSocketService extends BaseStockSocketDomainService { + private readonly logger = new Logger(); + + constructor( + protected readonly socketGateway: SocketGateway, + protected readonly baseSocketDomainService: BaseSocketDomainService, + private readonly stockExecuteOrderRepository: StockExecuteOrderRepository, + ) { + super(socketGateway, baseSocketDomainService, 'H0STCNT0'); + } + + async socketOpenHandler(): Promise { + const orders: Order[] = + await this.stockExecuteOrderRepository.findAllCodeByStatus(); + orders.forEach((order) => { + this.baseSocketDomainService.registerCode(this.TR_ID, order.stock_code); + }); + } + + socketDataHandler(data: string[]) { + this.checkExecutableOrder( + data[0], // 주식 코드 + data[2], // 주식 체결가 + ).catch((err) => { + throw new InternalServerErrorException(err); + }); + + const tradeData: TodayStockTradeHistoryDataDto = { + stck_shrn_iscd: data[0], + stck_cntg_hour: data[1], + stck_prpr: data[2], + prdy_vrss_sign: data[3], + cntg_vol: data[12], + prdy_ctrt: data[5], + }; + + const detailData: StockDetailSocketDataDto = { + stck_prpr: data[2], + prdy_vrss_sign: data[3], + prdy_vrss: data[4], + prdy_ctrt: data[5], + }; + + this.socketGateway.sendStockIndexValueToClient( + `trade-history/${data[0]}`, + tradeData, + ); + + this.socketGateway.sendStockIndexValueToClient( + `detail/${data[0]}`, + detailData, + ); + } + + subscribeByCode(trKey: string) { + this.baseSocketDomainService.registerCode(this.TR_ID, trKey); + } + + unsubscribeByCode(trKey: string) { + this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); + } + + private async checkExecutableOrder(stockCode: string, value) { + const buyOrders = await this.stockExecuteOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.BUY, + status: StatusType.PENDING, + price: MoreThanOrEqual(value), + }, + }); + + const sellOrders = await this.stockExecuteOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.SELL, + status: StatusType.PENDING, + price: LessThanOrEqual(value), + }, + }); + + await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); + await Promise.all( + sellOrders.map((sellOrder) => this.executeSell(sellOrder)), + ); + + if ( + !(await this.stockExecuteOrderRepository.existsBy({ + stock_code: stockCode, + status: StatusType.PENDING, + })) + ) + this.unsubscribeByCode(stockCode); + } + + private async executeBuy(order) { + this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockExecuteOrderRepository.updateOrderAndAssetAndUserStockWhenBuy( + order, + totalPrice + fee, + ); + } + + private async executeSell(order) { + this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockExecuteOrderRepository.updateOrderAndAssetAndUserStockWhenSell( + order, + totalPrice - fee, + ); + } + + private calculateFee(totalPrice: number) { + if (totalPrice <= 10000000) return Math.floor(totalPrice * 0.16); + if (totalPrice > 10000000 && totalPrice <= 50000000) + return Math.floor(totalPrice * 0.14); + if (totalPrice > 50000000 && totalPrice <= 100000000) + return Math.floor(totalPrice * 0.12); + if (totalPrice > 100000000 && totalPrice <= 300000000) + return Math.floor(totalPrice * 0.1); + return Math.floor(totalPrice * 0.08); + } +} diff --git a/BE/src/stockSocket/stock-socket.module.ts b/BE/src/stockSocket/stock-socket.module.ts new file mode 100644 index 00000000..fd2dc094 --- /dev/null +++ b/BE/src/stockSocket/stock-socket.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { StockIndexSocketService } from './stock-index-socket.service'; +import { StockPriceSocketService } from './stock-price-socket.service'; +import { SocketModule } from '../common/websocket/socket.module'; +import { StockExecuteOrderRepository } from './stock-execute-order.repository'; + +@Module({ + imports: [SocketModule], + providers: [ + StockIndexSocketService, + StockPriceSocketService, + StockExecuteOrderRepository, + ], + exports: [StockIndexSocketService, StockPriceSocketService], +}) +export class StockSocketModule {} From c1a1f0044cddbbde16d821b598b50c5c59ae2842 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 13:57:52 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=E2=99=BB=20refactor:=20=EB=8B=A4=EB=A5=B8?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=EC=84=9C=20stockSocket?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=EC=9D=98=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EC=B0=B8=EC=A1=B0=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.module.ts | 4 +- BE/src/asset/asset.service.ts | 10 +- BE/src/common/websocket/socket.gateway.ts | 4 - .../stock/index/stock-index-socket.service.ts | 42 ----- BE/src/stock/index/stock-index.module.ts | 3 +- .../stock/order/stock-order-socket.service.ts | 119 ------------- BE/src/stock/order/stock-order.module.ts | 11 +- BE/src/stock/order/stock-order.repository.ts | 85 --------- BE/src/stock/order/stock-order.service.ts | 12 +- .../stock-trade-history-socket.service.ts | 162 ------------------ .../history/stock-trade-history.controller.ts | 73 ++++---- .../history/stock-trade-history.module.ts | 7 +- .../history/stock-trade-history.service.ts | 7 + 13 files changed, 63 insertions(+), 476 deletions(-) delete mode 100644 BE/src/stock/index/stock-index-socket.service.ts delete mode 100644 BE/src/stock/order/stock-order-socket.service.ts delete mode 100644 BE/src/stock/trade/history/stock-trade-history-socket.service.ts diff --git a/BE/src/asset/asset.module.ts b/BE/src/asset/asset.module.ts index db1dc8ad..de73a73e 100644 --- a/BE/src/asset/asset.module.ts +++ b/BE/src/asset/asset.module.ts @@ -7,13 +7,13 @@ import { Asset } from './asset.entity'; import { UserStock } from './user-stock.entity'; import { UserStockRepository } from './user-stock.repository'; import { StockDetailModule } from '../stock/detail/stock-detail.module'; -import { StockTradeHistoryModule } from '../stock/trade/history/stock-trade-history.module'; +import { StockSocketModule } from '../stockSocket/stock-socket.module'; @Module({ imports: [ TypeOrmModule.forFeature([Asset, UserStock]), StockDetailModule, - StockTradeHistoryModule, + StockSocketModule, ], controllers: [AssetController], providers: [AssetService, AssetRepository, UserStockRepository], diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 2471c29b..ce4a8f88 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -8,8 +8,8 @@ import { StockDetailService } from '../stock/detail/stock-detail.service'; import { UserStock } from './user-stock.entity'; import { Asset } from './asset.entity'; import { InquirePriceResponseDto } from '../stock/detail/dto/stock-detail-response.dto'; -import { StockTradeHistorySocketService } from '../stock/trade/history/stock-trade-history-socket.service'; import { TradeType } from '../stock/order/enum/trade-type'; +import { StockPriceSocketService } from '../stockSocket/stock-price-socket.service'; @Injectable() export class AssetService { @@ -17,7 +17,7 @@ export class AssetService { private readonly userStockRepository: UserStockRepository, private readonly assetRepository: AssetRepository, private readonly stockDetailService: StockDetailService, - private readonly stockTradeHistorySocketService: StockTradeHistorySocketService, + private readonly stockPriceSocketService: StockPriceSocketService, ) {} async getUserStockByCode(userId: number, stockCode: string) { @@ -148,7 +148,7 @@ export class AssetService { await this.userStockRepository.findAllDistinctCode(userId); userStocks.map((userStock) => - this.stockTradeHistorySocketService.subscribeByCode(userStock.stock_code), + this.stockPriceSocketService.subscribeByCode(userStock.stock_code), ); } @@ -157,9 +157,7 @@ export class AssetService { await this.userStockRepository.findAllDistinctCode(userId); userStocks.map((userStock) => - this.stockTradeHistorySocketService.unsubscribeByCode( - userStock.stock_code, - ), + this.stockPriceSocketService.unsubscribeByCode(userStock.stock_code), ); } } diff --git a/BE/src/common/websocket/socket.gateway.ts b/BE/src/common/websocket/socket.gateway.ts index 48668008..d533c3f0 100644 --- a/BE/src/common/websocket/socket.gateway.ts +++ b/BE/src/common/websocket/socket.gateway.ts @@ -20,8 +20,4 @@ export class SocketGateway { this.server.emit(event, stockIndexValue); } - - sendStockTradeHistoryValueToClient(event, historyData) { - this.server.emit(event, historyData); - } } diff --git a/BE/src/stock/index/stock-index-socket.service.ts b/BE/src/stock/index/stock-index-socket.service.ts deleted file mode 100644 index 05ecb307..00000000 --- a/BE/src/stock/index/stock-index-socket.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { StockIndexValueElementDto } from './dto/stock-index-value-element.dto'; -import { BaseSocketDomainService } from '../../common/websocket/base-socket.domain-service'; -import { SocketGateway } from '../../common/websocket/socket.gateway'; - -@Injectable() -export class StockIndexSocketService { - private TR_ID = 'H0UPCNT0'; - private STOCK_CODE = { - '0001': 'KOSPI', - '1001': 'KOSDAQ', - '2001': 'KOSPI200', - '3003': 'KSQ150', - }; - - constructor( - private readonly socketGateway: SocketGateway, - private readonly baseSocketDomainService: BaseSocketDomainService, - ) { - baseSocketDomainService.registerSocketOpenHandler(() => { - this.baseSocketDomainService.registerCode(this.TR_ID, '0001'); // 코스피 - this.baseSocketDomainService.registerCode(this.TR_ID, '1001'); // 코스닥 - this.baseSocketDomainService.registerCode(this.TR_ID, '2001'); // 코스피200 - this.baseSocketDomainService.registerCode(this.TR_ID, '3003'); // KSQ150 - }); - - baseSocketDomainService.registerSocketDataHandler( - this.TR_ID, - (data: string[]) => { - this.socketGateway.sendStockIndexValueToClient( - this.STOCK_CODE[data[0]], - new StockIndexValueElementDto( - data[2], // 주가 지수 - data[4], // 전일 대비 등락 - data[9], // 전일 대비 등락률 - data[3], // 부호 - ), - ); - }, - ); - } -} diff --git a/BE/src/stock/index/stock-index.module.ts b/BE/src/stock/index/stock-index.module.ts index dd3d21f4..186a5b05 100644 --- a/BE/src/stock/index/stock-index.module.ts +++ b/BE/src/stock/index/stock-index.module.ts @@ -3,11 +3,10 @@ import { StockIndexController } from './stock-index.controller'; import { StockIndexService } from './stock-index.service'; import { KoreaInvestmentModule } from '../../common/koreaInvestment/korea-investment.module'; import { SocketModule } from '../../common/websocket/socket.module'; -import { StockIndexSocketService } from './stock-index-socket.service'; @Module({ imports: [KoreaInvestmentModule, SocketModule], controllers: [StockIndexController], - providers: [StockIndexService, StockIndexSocketService], + providers: [StockIndexService], }) export class StockIndexModule {} diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts deleted file mode 100644 index 5b1b5d5a..00000000 --- a/BE/src/stock/order/stock-order-socket.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - Injectable, - InternalServerErrorException, - Logger, -} from '@nestjs/common'; -import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; -import { BaseSocketDomainService } from '../../common/websocket/base-socket.domain-service'; -import { SocketGateway } from '../../common/websocket/socket.gateway'; -import { Order } from './stock-order.entity'; -import { TradeType } from './enum/trade-type'; -import { StatusType } from './enum/status-type'; -import { StockOrderRepository } from './stock-order.repository'; - -@Injectable() -export class StockOrderSocketService { - private TR_ID = 'H0STCNT0'; - - private readonly logger = new Logger(); - - constructor( - private readonly socketGateway: SocketGateway, - private readonly baseSocketDomainService: BaseSocketDomainService, - private readonly stockOrderRepository: StockOrderRepository, - ) { - baseSocketDomainService.registerSocketOpenHandler(async () => { - const orders: Order[] = - await this.stockOrderRepository.findAllCodeByStatus(); - orders.forEach((order) => { - baseSocketDomainService.registerCode(this.TR_ID, order.stock_code); - }); - }); - - baseSocketDomainService.registerSocketDataHandler( - this.TR_ID, - (data: string[]) => { - this.checkExecutableOrder( - data[0], // 주식 코드 - data[2], // 주식 체결가 - ).catch((err) => { - throw new InternalServerErrorException(err); - }); - }, - ); - } - - subscribeByCode(trKey: string) { - this.baseSocketDomainService.registerCode(this.TR_ID, trKey); - } - - unsubscribeByCode(trKey: string) { - this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); - } - - private async checkExecutableOrder(stockCode: string, value) { - const buyOrders = await this.stockOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.BUY, - status: StatusType.PENDING, - price: MoreThanOrEqual(value), - }, - }); - - const sellOrders = await this.stockOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.SELL, - status: StatusType.PENDING, - price: LessThanOrEqual(value), - }, - }); - - await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); - await Promise.all( - sellOrders.map((sellOrder) => this.executeSell(sellOrder)), - ); - - if ( - !(await this.stockOrderRepository.existsBy({ - stock_code: stockCode, - status: StatusType.PENDING, - })) - ) - this.unsubscribeByCode(stockCode); - } - - private async executeBuy(order) { - this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetAndUserStockWhenBuy( - order, - totalPrice + fee, - ); - } - - private async executeSell(order) { - this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetAndUserStockWhenSell( - order, - totalPrice - fee, - ); - } - - private calculateFee(totalPrice: number) { - if (totalPrice <= 10000000) return Math.floor(totalPrice * 0.16); - if (totalPrice > 10000000 && totalPrice <= 50000000) - return Math.floor(totalPrice * 0.14); - if (totalPrice > 50000000 && totalPrice <= 100000000) - return Math.floor(totalPrice * 0.12); - if (totalPrice > 100000000 && totalPrice <= 300000000) - return Math.floor(totalPrice * 0.1); - return Math.floor(totalPrice * 0.08); - } -} diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index 375d4a53..69bda237 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -6,11 +6,16 @@ import { Order } from './stock-order.entity'; import { StockOrderRepository } from './stock-order.repository'; import { SocketModule } from '../../common/websocket/socket.module'; import { AssetModule } from '../../asset/asset.module'; -import { StockOrderSocketService } from './stock-order-socket.service'; +import { StockSocketModule } from '../../stockSocket/stock-socket.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order]), SocketModule, AssetModule], + imports: [ + TypeOrmModule.forFeature([Order]), + SocketModule, + AssetModule, + StockSocketModule, + ], controllers: [StockOrderController], - providers: [StockOrderService, StockOrderRepository, StockOrderSocketService], + providers: [StockOrderService, StockOrderRepository], }) export class StockOrderModule {} diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 92da1ce6..84c453ee 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -20,91 +20,6 @@ export class StockOrderRepository extends Repository { .getRawMany(); } - async updateOrderAndAssetAndUserStockWhenBuy(order, realPrice) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction(); - - try { - await queryRunner.manager.update( - Order, - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, - ); - await queryRunner.manager - .createQueryBuilder() - .update(Asset) - .set({ - cash_balance: () => `cash_balance - :realPrice`, - last_updated: new Date(), - }) - .where({ user_id: order.user_id }) - .setParameters({ realPrice }) - .execute(); - - await queryRunner.query( - `INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE avg_price = (avg_price * quantity + ? * ?) / (quantity + ?), quantity = quantity + ?`, - [ - order.user_id, - order.stock_code, - order.amount, - order.price, - new Date(), - order.price, - order.amount, - order.amount, - order.amount, - ], - ); - - await queryRunner.commitTransaction(); - } catch (err) { - await queryRunner.rollbackTransaction(); - throw new InternalServerErrorException(err); - } finally { - await queryRunner.release(); - } - } - - async updateOrderAndAssetAndUserStockWhenSell(order, realPrice) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction(); - - try { - await queryRunner.manager.update( - Order, - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, - ); - await queryRunner.manager - .createQueryBuilder() - .update(Asset) - .set({ - cash_balance: () => `cash_balance + :realPrice`, - last_updated: new Date(), - }) - .where({ user_id: order.user_id }) - .setParameters({ realPrice }) - .execute(); - - await queryRunner.manager - .createQueryBuilder() - .update(UserStock) - .set({ - quantity: () => `quantity - :newQuantity`, - }) - .where({ user_id: order.user_id, stock_code: order.stock_code }) - .setParameters({ newQuantity: order.amount }) - .execute(); - - await queryRunner.commitTransaction(); - } catch (err) { - await queryRunner.rollbackTransaction(); - throw new InternalServerErrorException(); - } finally { - await queryRunner.release(); - } - } - async findAllPendingOrdersByUserId(userId: number) { return this.createQueryBuilder('o') .leftJoinAndSelect('stocks', 's', 's.code = o.stock_code') diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index bbc4572f..73879bca 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -9,17 +9,17 @@ import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; import { StatusType } from './enum/status-type'; -import { StockOrderSocketService } from './stock-order-socket.service'; import { UserStockRepository } from '../../asset/user-stock.repository'; import { AssetRepository } from '../../asset/asset.repository'; import { StockOrderElementResponseDto } from './dto/stock-order-element-response.dto'; import { Order } from './stock-order.entity'; +import { StockPriceSocketService } from '../../stockSocket/stock-price-socket.service'; @Injectable() export class StockOrderService { constructor( private readonly stockOrderRepository: StockOrderRepository, - private readonly stockOrderSocketService: StockOrderSocketService, + private readonly stockPriceSocketService: StockPriceSocketService, private readonly userStockRepository: UserStockRepository, private readonly assetRepository: AssetRepository, ) {} @@ -54,7 +54,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); - this.stockOrderSocketService.subscribeByCode(stockOrderRequest.stock_code); + this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); } async sell(userId: number, stockOrderRequest: StockOrderRequestDto) { @@ -89,7 +89,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); - this.stockOrderSocketService.subscribeByCode(stockOrderRequest.stock_code); + this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); } async cancel(userId: number, orderId: number) { @@ -111,7 +111,7 @@ export class StockOrderService { status: StatusType.PENDING, })) ) - this.stockOrderSocketService.unsubscribeByCode(order.stock_code); + this.stockPriceSocketService.unsubscribeByCode(order.stock_code); } async getPendingListByUserId(userId: number) { @@ -137,7 +137,7 @@ export class StockOrderService { await Promise.all( orders.map((order) => - this.stockOrderSocketService.unsubscribeByCode(order.stock_code), + this.stockPriceSocketService.unsubscribeByCode(order.stock_code), ), ); diff --git a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts deleted file mode 100644 index b32f9601..00000000 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { WebSocket } from 'ws'; -import axios from 'axios'; -import { filter, map, Observable, Subject } from 'rxjs'; -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { SseEvent } from './interface/sse-event'; -import { SocketConnectTokenInterface } from '../../../common/websocket/interface/socket.interface'; -import { getFullTestURL } from '../../../util/get-full-URL'; -import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; -import { SocketGateway } from '../../../common/websocket/socket.gateway'; -import { StockDetailSocketDataDto } from './dto/stock-detail-socket-data.dto'; - -@Injectable() -export class StockTradeHistorySocketService implements OnModuleInit { - private readonly logger = new Logger(''); - private socket: WebSocket; - private socketConnectionKey: string; - private subscribedStocks = new Set(); - private TR_ID = 'H0STCNT0'; - private eventSubject = new Subject(); - - constructor(private readonly socketGateway: SocketGateway) {} - - async onModuleInit() { - this.socketConnectionKey = await this.getSocketConnectionKey(); - this.socket = new WebSocket(process.env.KOREA_INVESTMENT_TEST_SOCKET_URL); - - this.socket.onopen = () => {}; - - this.socket.onmessage = (event) => { - const data = - typeof event.data === 'string' - ? event.data.split('|') - : JSON.stringify(event.data); - - if (data.length < 2) { - const json = JSON.parse(data[0]); - if (json.body) - this.logger.log( - `한국투자증권 웹소켓 연결: ${json.body.msg1}`, - json.header.tr_id, - ); - if (json.header.tr_id === 'PINGPONG') - this.socket.pong(JSON.stringify(json)); - return; - } - - const dataList = data[3].split('^'); - - const tradeData: TodayStockTradeHistoryDataDto = { - stck_shrn_iscd: dataList[0], - stck_cntg_hour: dataList[1], - stck_prpr: dataList[2], - prdy_vrss_sign: dataList[3], - cntg_vol: dataList[12], - prdy_ctrt: dataList[5], - }; - - const detailData: StockDetailSocketDataDto = { - stck_prpr: dataList[2], - prdy_vrss_sign: dataList[3], - prdy_vrss: dataList[4], - prdy_ctrt: dataList[5], - }; - - this.eventSubject.next({ - data: JSON.stringify({ - tradeData, - }), - }); - - this.socketGateway.sendStockTradeHistoryValueToClient( - `trade-history/${dataList[0]}`, - tradeData, - ); - - this.socketGateway.sendStockIndexValueToClient( - `detail/${dataList[0]}`, - detailData, - ); - }; - - this.socket.onclose = () => { - this.logger.warn(`한국투자증권 소켓 연결 종료`); - }; - } - - getTradeDataStream(targetStockCode: string): Observable { - return this.eventSubject.pipe( - filter((event: SseEvent) => { - const parsed = JSON.parse(event.data); - return parsed.tradeData.stck_shrn_iscd === targetStockCode; - }), - map((event: SseEvent) => event), - ); - } - - subscribeByCode(stockCode: string) { - this.registerCode(this.TR_ID, stockCode); - this.subscribedStocks.add(stockCode); - } - - unsubscribeByCode(stockCode: string) { - this.unregisterCode(this.TR_ID, stockCode); - this.subscribedStocks.delete(stockCode); - } - - registerCode(trId: string, trKey: string) { - this.socket.send( - JSON.stringify({ - header: { - approval_key: this.socketConnectionKey, - custtype: 'P', - tr_type: '1', - 'content-type': 'utf-8', - }, - body: { - input: { - tr_id: trId, - tr_key: trKey, - }, - }, - }), - ); - } - - unregisterCode(trId: string, trKey: string) { - this.socket.send( - JSON.stringify({ - header: { - approval_key: this.socketConnectionKey, - custtype: 'P', - tr_type: '2', - 'content-type': 'utf-8', - }, - body: { - input: { - tr_id: trId, - tr_key: trKey, - }, - }, - }), - ); - } - - async getSocketConnectionKey() { - if (this.socketConnectionKey) { - return this.socketConnectionKey; - } - - const response = await axios.post( - getFullTestURL('/oauth2/Approval'), - { - grant_type: 'client_credentials', - appkey: process.env.KOREA_INVESTMENT_TEST_APP_KEY, - secretkey: process.env.KOREA_INVESTMENT_TEST_APP_SECRET, - }, - ); - - this.socketConnectionKey = response.data.approval_key; - return this.socketConnectionKey; - } -} diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index b225642d..46c25762 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -1,18 +1,14 @@ -import { Observable } from 'rxjs'; -import { Controller, Get, Param, Sse } from '@nestjs/common'; +import { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; -import { StockTradeHistorySocketService } from './stock-trade-history-socket.service'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; -import { SseEvent } from './interface/sse-event'; @ApiTags('주식현재가 체결 조회 API') @Controller('/api/stocks/trade-history') export class StockTradeHistoryController { constructor( private readonly stockTradeHistoryService: StockTradeHistoryService, - private readonly stockTradeHistorySocketService: StockTradeHistorySocketService, ) {} @Get(':stockCode/today') @@ -30,12 +26,7 @@ export class StockTradeHistoryController { type: TodayStockTradeHistoryDataDto, }) getTodayStockTradeHistory(@Param('stockCode') stockCode: string) { - const data = - this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); - - this.stockTradeHistorySocketService.subscribeByCode(stockCode); - - return data; + return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } @Get(':stockCode/daily') @@ -56,35 +47,35 @@ export class StockTradeHistoryController { return this.stockTradeHistoryService.getDailyStockTradeHistory(stockCode); } - @Sse(':stockCode/today-sse') - @ApiOperation({ summary: '단일 주식 종목에 대한 실시간 체결 데이터 스트림' }) - @ApiParam({ - name: 'stockCode', - required: true, - description: - '종목 코드\n\n' + - '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', - }) - @ApiResponse({ - status: 200, - description: - '단일 주식 종목에 대한 주식현재가 체결값 실시간 데이터 조회 성공', - type: TodayStockTradeHistoryDataDto, - }) - streamTradeHistory(@Param('stockCode') stockCode: string) { - this.stockTradeHistorySocketService.subscribeByCode(stockCode); - - return new Observable((subscriber) => { - const subscription = this.stockTradeHistorySocketService - .getTradeDataStream(stockCode) - .subscribe(subscriber); - - return () => { - this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); - subscription.unsubscribe(); - }; - }); - } + // @Sse(':stockCode/today-sse') + // @ApiOperation({ summary: '단일 주식 종목에 대한 실시간 체결 데이터 스트림' }) + // @ApiParam({ + // name: 'stockCode', + // required: true, + // description: + // '종목 코드\n\n' + + // '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + // }) + // @ApiResponse({ + // status: 200, + // description: + // '단일 주식 종목에 대한 주식현재가 체결값 실시간 데이터 조회 성공', + // type: TodayStockTradeHistoryDataDto, + // }) + // streamTradeHistory(@Param('stockCode') stockCode: string) { + // this.stockTradeHistorySocketService.subscribeByCode(stockCode); + // + // return new Observable((subscriber) => { + // const subscription = this.stockTradeHistorySocketService + // .getTradeDataStream(stockCode) + // .subscribe(subscriber); + // + // return () => { + // this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); + // subscription.unsubscribe(); + // }; + // }); + // } @Get(':stockCode/unsubscribe') @ApiOperation({ summary: '페이지를 벗어날 때 구독을 취소하기 위한 API' }) @@ -100,6 +91,6 @@ export class StockTradeHistoryController { description: '구독 취소 성공', }) unsubscribeCode(@Param('stockCode') stockCode: string) { - this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); + this.stockTradeHistoryService.unsubscribeCode(stockCode); } } diff --git a/BE/src/stock/trade/history/stock-trade-history.module.ts b/BE/src/stock/trade/history/stock-trade-history.module.ts index bcf067be..a3c55fd1 100644 --- a/BE/src/stock/trade/history/stock-trade-history.module.ts +++ b/BE/src/stock/trade/history/stock-trade-history.module.ts @@ -2,13 +2,12 @@ import { Module } from '@nestjs/common'; import { KoreaInvestmentModule } from '../../../common/koreaInvestment/korea-investment.module'; import { StockTradeHistoryController } from './stock-trade-history.controller'; import { StockTradeHistoryService } from './stock-trade-history.service'; -import { StockTradeHistorySocketService } from './stock-trade-history-socket.service'; import { SocketModule } from '../../../common/websocket/socket.module'; +import { StockSocketModule } from '../../../stockSocket/stock-socket.module'; @Module({ - imports: [KoreaInvestmentModule, SocketModule], + imports: [KoreaInvestmentModule, SocketModule, StockSocketModule], controllers: [StockTradeHistoryController], - providers: [StockTradeHistoryService, StockTradeHistorySocketService], - exports: [StockTradeHistorySocketService], + providers: [StockTradeHistoryService], }) export class StockTradeHistoryModule {} diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index 564ba4ca..9a3e2a7c 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -6,11 +6,13 @@ import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-d import { InquireDailyPriceApiResponse } from './interface/inquire-daily-price.interface'; import { DailyStockTradeHistoryOutputDto } from './dto/daily-stock-trade-history-ouput.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; +import { StockPriceSocketService } from '../../../stockSocket/stock-price-socket.service'; @Injectable() export class StockTradeHistoryService { constructor( private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, + private readonly stockPriceSocketService: StockPriceSocketService, ) {} /** @@ -33,6 +35,7 @@ export class StockTradeHistoryService { queryParams, ); + this.stockPriceSocketService.subscribeByCode(stockCode); return this.formatTodayStockTradeHistoryData(response.output); } @@ -107,4 +110,8 @@ export class StockTradeHistoryService { return historyData; }); } + + unsubscribeCode(stockCode: string) { + return this.stockPriceSocketService.unsubscribeByCode(stockCode); + } } From f1205446b645734a3767eb72dc9ce5f8e9d3fe8d Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 14:40:02 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=B9=B4=EC=9A=B4=ED=8A=B8=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EC=97=B0=EA=B2=B0=20=EA=B4=80=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 4 ++-- BE/src/stockSocket/stock-price-socket.service.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/BE/src/main.ts b/BE/src/main.ts index 7f404bf8..febd76f1 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -13,10 +13,10 @@ async function bootstrap() { 'http://localhost:5173', 'http://juga.kro.kr:5173', 'http://juga.kro.kr:3000', - //개발 서버 + // 개발 서버 'http://223.130.151.42:5173', 'http://223.130.151.42:3000', - //배포 서버 + // 배포 서버 'http://175.45.204.158', 'http://175.45.204.158:3000', 'http://juga.kro.kr', diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts index 60695e13..f161c92b 100644 --- a/BE/src/stockSocket/stock-price-socket.service.ts +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -17,6 +17,7 @@ import { StockExecuteOrderRepository } from './stock-execute-order.repository'; @Injectable() export class StockPriceSocketService extends BaseStockSocketDomainService { private readonly logger = new Logger(); + private connection: { [key: string]: number } = {}; constructor( protected readonly socketGateway: SocketGateway, @@ -71,10 +72,16 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { subscribeByCode(trKey: string) { this.baseSocketDomainService.registerCode(this.TR_ID, trKey); + + if (this.connection[trKey]) return this.connection[trKey]++; + return (this.connection[trKey] = 1); } unsubscribeByCode(trKey: string) { - this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); + if (!this.connection[trKey]) return; + if (this.connection[trKey] > 1) return this.connection[trKey]--; + delete this.connection[trKey]; + return this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); } private async checkExecutableOrder(stockCode: string, value) { @@ -102,6 +109,7 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { ); if ( + buyOrders.length + sellOrders.length > 0 && !(await this.stockExecuteOrderRepository.existsBy({ stock_code: stockCode, status: StatusType.PENDING, From 63c7bc2e984b58e2152e5467f4906d3473e46534 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 15:03:10 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=94=A7=20fix:=20lint=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 2 +- BE/src/stock/order/stock-order.repository.ts | 4 +--- BE/src/stockSocket/stock-price-socket.service.ts | 14 ++++++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index edf1d064..5cdd0a05 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -98,7 +98,7 @@ export class RankingService { return { topRank: parsedTopRank, - userRank: userRank, + userRank, }; } diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 84c453ee..c79abd48 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -1,10 +1,8 @@ import { DataSource, Repository } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Order } from './stock-order.entity'; import { StatusType } from './enum/status-type'; -import { Asset } from '../../asset/asset.entity'; -import { UserStock } from '../../asset/user-stock.entity'; import { StockOrderRawInterface } from './interface/stock-order-raw.interface'; @Injectable() diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts index f161c92b..19348a6b 100644 --- a/BE/src/stockSocket/stock-price-socket.service.ts +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -73,15 +73,21 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { subscribeByCode(trKey: string) { this.baseSocketDomainService.registerCode(this.TR_ID, trKey); - if (this.connection[trKey]) return this.connection[trKey]++; - return (this.connection[trKey] = 1); + if (this.connection[trKey]) { + this.connection[trKey] += 1; + return; + } + this.connection[trKey] = 1; } unsubscribeByCode(trKey: string) { if (!this.connection[trKey]) return; - if (this.connection[trKey] > 1) return this.connection[trKey]--; + if (this.connection[trKey] > 1) { + this.connection[trKey] -= 1; + return; + } delete this.connection[trKey]; - return this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); + this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); } private async checkExecutableOrder(stockCode: string, value) { From 66b38e1c10b0198adc8f0d2ff4a680cfef351ebd Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 27 Nov 2024 10:42:56 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=B2=98=EB=A6=AC=20=EB=90=98=EC=96=B4=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20SSE=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../history/stock-trade-history.controller.ts | 64 ++++++++++--------- .../stockSocket/stock-price-socket.service.ts | 19 ++++++ 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index 46c25762..c003aabb 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -1,14 +1,18 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Controller, Get, Param, Sse } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; +import { Observable } from 'rxjs'; +import { SseEvent } from './interface/sse-event'; +import { StockPriceSocketService } from '../../../stockSocket/stock-price-socket.service'; @ApiTags('주식현재가 체결 조회 API') @Controller('/api/stocks/trade-history') export class StockTradeHistoryController { constructor( private readonly stockTradeHistoryService: StockTradeHistoryService, + private readonly stockPriceSocketService: StockPriceSocketService, ) {} @Get(':stockCode/today') @@ -47,35 +51,35 @@ export class StockTradeHistoryController { return this.stockTradeHistoryService.getDailyStockTradeHistory(stockCode); } - // @Sse(':stockCode/today-sse') - // @ApiOperation({ summary: '단일 주식 종목에 대한 실시간 체결 데이터 스트림' }) - // @ApiParam({ - // name: 'stockCode', - // required: true, - // description: - // '종목 코드\n\n' + - // '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', - // }) - // @ApiResponse({ - // status: 200, - // description: - // '단일 주식 종목에 대한 주식현재가 체결값 실시간 데이터 조회 성공', - // type: TodayStockTradeHistoryDataDto, - // }) - // streamTradeHistory(@Param('stockCode') stockCode: string) { - // this.stockTradeHistorySocketService.subscribeByCode(stockCode); - // - // return new Observable((subscriber) => { - // const subscription = this.stockTradeHistorySocketService - // .getTradeDataStream(stockCode) - // .subscribe(subscriber); - // - // return () => { - // this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); - // subscription.unsubscribe(); - // }; - // }); - // } + @Sse(':stockCode/today-sse') + @ApiOperation({ summary: '단일 주식 종목에 대한 실시간 체결 데이터 스트림' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: + '단일 주식 종목에 대한 주식현재가 체결값 실시간 데이터 조회 성공', + type: TodayStockTradeHistoryDataDto, + }) + streamTradeHistory(@Param('stockCode') stockCode: string) { + this.stockPriceSocketService.subscribeByCode(stockCode); + + return new Observable((subscriber) => { + const subscription = this.stockPriceSocketService + .getTradeDataStream(stockCode) + .subscribe(subscriber); + + return () => { + this.stockPriceSocketService.unsubscribeByCode(stockCode); + subscription.unsubscribe(); + }; + }); + } @Get(':stockCode/unsubscribe') @ApiOperation({ summary: '페이지를 벗어날 때 구독을 취소하기 위한 API' }) diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts index 19348a6b..6744d9f3 100644 --- a/BE/src/stockSocket/stock-price-socket.service.ts +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -4,6 +4,7 @@ import { Logger, } from '@nestjs/common'; import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { filter, map, Observable, Subject } from 'rxjs'; import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; import { SocketGateway } from '../common/websocket/socket.gateway'; import { BaseStockSocketDomainService } from './base-stock-socket.domain-service'; @@ -13,11 +14,13 @@ import { StatusType } from '../stock/order/enum/status-type'; import { TodayStockTradeHistoryDataDto } from '../stock/trade/history/dto/today-stock-trade-history-data.dto'; import { StockDetailSocketDataDto } from '../stock/trade/history/dto/stock-detail-socket-data.dto'; import { StockExecuteOrderRepository } from './stock-execute-order.repository'; +import { SseEvent } from '../stock/trade/history/interface/sse-event'; @Injectable() export class StockPriceSocketService extends BaseStockSocketDomainService { private readonly logger = new Logger(); private connection: { [key: string]: number } = {}; + private eventSubject = new Subject(); constructor( protected readonly socketGateway: SocketGateway, @@ -59,6 +62,12 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { prdy_ctrt: data[5], }; + this.eventSubject.next({ + data: JSON.stringify({ + tradeData, + }), + }); + this.socketGateway.sendStockIndexValueToClient( `trade-history/${data[0]}`, tradeData, @@ -70,6 +79,16 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { ); } + getTradeDataStream(targetStockCode: string): Observable { + return this.eventSubject.pipe( + filter((event: SseEvent) => { + const parsed = JSON.parse(event.data); + return parsed.tradeData.stck_shrn_iscd === targetStockCode; + }), + map((event: SseEvent) => event), + ); + } + subscribeByCode(trKey: string) { this.baseSocketDomainService.registerCode(this.TR_ID, trKey); From 1f8a2f3bd993733c0c7fe6481463e8b828323f2c Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 27 Nov 2024 10:46:20 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=94=A7=20fix:=20lint=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/trade/history/stock-trade-history.controller.ts | 4 ++-- BE/src/stock/trade/history/stock-trade-history.service.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index c003aabb..4d17d6f9 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -1,9 +1,9 @@ +import { Observable } from 'rxjs'; import { Controller, Get, Param, Sse } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; -import { Observable } from 'rxjs'; import { SseEvent } from './interface/sse-event'; import { StockPriceSocketService } from '../../../stockSocket/stock-price-socket.service'; @@ -95,6 +95,6 @@ export class StockTradeHistoryController { description: '구독 취소 성공', }) unsubscribeCode(@Param('stockCode') stockCode: string) { - this.stockTradeHistoryService.unsubscribeCode(stockCode); + this.stockPriceSocketService.unsubscribeByCode(stockCode); } } diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index 9a3e2a7c..8ef3b26c 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -111,7 +111,7 @@ export class StockTradeHistoryService { }); } - unsubscribeCode(stockCode: string) { - return this.stockPriceSocketService.unsubscribeByCode(stockCode); - } + // unsubscribeCode(stockCode: string) { + // return this.stockPriceSocketService.unsubscribeByCode(stockCode); + // } }