From edb326f6007dfd7b75e4158d0cf38188c8b17b58 Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 14 Nov 2024 16:49:08 +0900 Subject: [PATCH 01/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B2=B4=EA=B2=B0=EA=B0=80=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=97=B0=EA=B2=B0=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1#56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock-trade-history-socket.service.ts | 53 +++++++++++++++++++ .../history/stock-trade-history.controller.ts | 3 ++ .../history/stock-trade-history.module.ts | 6 ++- BE/src/websocket/socket.gateway.ts | 4 ++ 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 BE/src/stock/trade/history/stock-trade-history-socket.service.ts 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 new file mode 100644 index 00000000..d97a818b --- /dev/null +++ b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts @@ -0,0 +1,53 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { SocketGateway } from '../../../websocket/socket.gateway'; +import { BaseSocketService } from '../../../websocket/base-socket.service'; + +@Injectable() +export class StockTradeHistorySocketService { + private TR_ID = 'H0STCNT0'; + private readonly logger = new Logger('StockTradeHistorySocketService'); + + constructor( + private readonly socketGateway: SocketGateway, + private readonly baseSocketService: BaseSocketService, + ) { + baseSocketService.registerSocketOpenHandler(() => { + this.logger.debug('trade-history 소켓 연결 성공'); + }); + + baseSocketService.registerSocketDataHandler( + this.TR_ID, + (dataList: string[]) => { + try { + // BaseSocketService에서 이미 split된 데이터가 옴 + const tradeData = { + stck_prpr: dataList[2], + cntg_vol: dataList[12], + prdy_ctrt: dataList[5], + stck_cntg_hour: dataList[1], + }; + + const eventName = `trade-history/${dataList[0]}`; + this.socketGateway.sendStockTradeHistoryValueToClient( + eventName, + tradeData, + ); + } catch (error) { + this.logger.error('Error processing trade data:', error); + this.logger.error('Raw data was:', dataList); + } + }, + ); + } + + subscribeByCode(stockCode: string) { + try { + this.logger.log(`Subscribing to stock: ${stockCode}`); + this.baseSocketService.registerCode(this.TR_ID, stockCode); + this.logger.log(`Successfully subscribed to stock: ${stockCode}`); + } catch (error) { + this.logger.error(`Failed to subscribe to stock ${stockCode}:`, error); + throw error; + } + } +} 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 4885aed2..f3850545 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -3,12 +3,14 @@ import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-history-response.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; +import { StockTradeHistorySocketService } from './stock-trade-history-socket.service'; @ApiTags('주식현재가 체결 조회 API') @Controller('/api/stocks') export class StockTradeHistoryController { constructor( private readonly stockTradeHistoryService: StockTradeHistoryService, + private readonly stockTradeHistorySocketService: StockTradeHistorySocketService, ) {} @Get(':stockCode/today-trade-history') @@ -26,6 +28,7 @@ export class StockTradeHistoryController { type: TodayStockTradeHistoryResponseDto, }) getTodayStockTradeHistory(@Param('stockCode') stockCode: string) { + this.stockTradeHistorySocketService.subscribeByCode(stockCode); return this.stockTradeHistoryService.getTodayStockTradeHistory(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 965889a1..c21563f3 100644 --- a/BE/src/stock/trade/history/stock-trade-history.module.ts +++ b/BE/src/stock/trade/history/stock-trade-history.module.ts @@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'; import { KoreaInvestmentModule } from '../../../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 '../../../websocket/socket.module'; @Module({ - imports: [KoreaInvestmentModule], + imports: [KoreaInvestmentModule, SocketModule], controllers: [StockTradeHistoryController], - providers: [StockTradeHistoryService], + providers: [StockTradeHistoryService, StockTradeHistorySocketService], }) export class StockTradeHistoryModule {} diff --git a/BE/src/websocket/socket.gateway.ts b/BE/src/websocket/socket.gateway.ts index d533c3f0..32ff997d 100644 --- a/BE/src/websocket/socket.gateway.ts +++ b/BE/src/websocket/socket.gateway.ts @@ -20,4 +20,8 @@ export class SocketGateway { this.server.emit(event, stockIndexValue); } + + sendStockTradeHistoryValueToClient(event, chartData) { + this.server.emit(event, chartData); + } } From b2664686fbe68be7b0420826980f70742245742d Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:14:35 +0900 Subject: [PATCH 02/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20status=20pending=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 6cd70c39..6db56d6f 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -15,6 +15,7 @@ export class StockOrderRepository extends Repository { async findAllCodeByStatus() { return this.createQueryBuilder('orders') .select('DISTINCT orders.stock_code') + .where({ status: StatusType.PENDING }) .getRawMany(); } From 57d1174277e2ad6154f44b50e306f0000f8edc8a Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:15:04 +0900 Subject: [PATCH 03/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4=EB=8F=84/?= =?UTF-8?q?=EB=A7=A4=EC=88=98=20=EC=B2=B4=EA=B2=B0=20=EC=8B=9C=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=AC=B8=20=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.repository.ts | 57 +++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 6db56d6f..2b419efa 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -34,38 +34,41 @@ export class StockOrderRepository extends Repository { .createQueryBuilder() .update(Asset) .set({ - cash_balance: () => `cash_balance - ${realPrice}`, - total_asset: () => `total_asset - ${realPrice}`, - total_profit: () => `total_profit - ${realPrice}`, + cash_balance: () => `cash_balance - :realPrice`, + total_asset: () => `total_asset - :realPrice`, + total_profit: () => `total_profit - :realPrice`, total_profit_rate: () => `total_profit / 10000000`, last_updated: new Date(), }) .where({ user_id: order.user_id }) + .setParameters({ realPrice }) .execute(); - await queryRunner.manager - .createQueryBuilder() - .insert() - .into(UserStock) - .values({ - user_id: order.user_id, - stock_code: order.stock_code, - quantity: order.amount, - avg_price: order.price, - }) - .orUpdate( - [ - `quantity = quantity + ${order.amount}`, - `avg_price = ((avg_price * quantity + ${order.price} * ${order.amount}) / (quantity + ${order.amount}))`, - ], - ['user_id', 'stock_code'], - ) - .execute(); + await queryRunner.query( + ` + INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + quantity = quantity + ?, + avg_price = (avg_price * quantity + ? * ?) / (quantity + ?) +`, + [ + order.user_id, + order.stock_code, + order.amount, + order.price, + new Date(), + order.amount, + order.amount, + order.price, + order.amount, + ], + ); await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); - throw new InternalServerErrorException(); + throw new InternalServerErrorException(err); } finally { await queryRunner.release(); } @@ -86,22 +89,24 @@ export class StockOrderRepository extends Repository { .createQueryBuilder() .update(Asset) .set({ - cash_balance: () => `cash_balance + ${realPrice}`, - total_asset: () => `total_asset + ${realPrice}`, - total_profit: () => `total_profit + ${realPrice}`, + cash_balance: () => `cash_balance + :realPrice`, + total_asset: () => `total_asset + :realPrice`, + total_profit: () => `total_profit + :realPrice`, total_profit_rate: () => `total_profit / 10000000`, last_updated: new Date(), }) .where({ user_id: order.user_id }) + .setParameters({ realPrice }) .execute(); await queryRunner.manager .createQueryBuilder() .update(UserStock) .set({ - quantity: () => `quantity - ${order.amount}`, + quantity: () => `quantity - :newQuantity`, }) .where({ user_id: order.user_id, stock_code: order.stock_code }) + .setParameters({ newQuantity: order.amount }) .execute(); await queryRunner.commitTransaction(); From 139bc4a1d97fb8414d887fb5f8bd490944fc7745 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:16:25 +0900 Subject: [PATCH 04/81] =?UTF-8?q?=E2=9E=95=20add:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=B3=B4=EC=9C=A0=20=EC=A3=BC=EC=8B=9D=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=9C=A0=EB=8B=88=ED=81=AC=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=B6=94=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/userStock/user-stock.entity.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BE/src/userStock/user-stock.entity.ts b/BE/src/userStock/user-stock.entity.ts index 60a1b169..a02bf7f3 100644 --- a/BE/src/userStock/user-stock.entity.ts +++ b/BE/src/userStock/user-stock.entity.ts @@ -2,10 +2,12 @@ import { Column, Entity, PrimaryGeneratedColumn, + Unique, UpdateDateColumn, } from 'typeorm'; @Entity('user_stocks') +@Unique(['user_id', 'stock_code']) export class UserStock { @PrimaryGeneratedColumn() id: number; From b2fb455550fd801fddb1245690864ab46211cbde Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:17:12 +0900 Subject: [PATCH 05/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20user=20id=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.controller.ts | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index 31960590..268f4786 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -14,10 +14,10 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; +import { Request } from 'express'; import { StockOrderService } from './stock-order.service'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { JwtAuthGuard } from '../../auth/jwt-auth-guard'; -import { RequestInterface } from './interface/request.interface'; @Controller('/api/stocks/trade') @ApiTags('주식 매수/매도 API') @@ -36,10 +36,13 @@ export class StockOrderController { description: '주식 매수 예약 등록 성공', }) async buy( - @Req() request: RequestInterface, + @Req() request: Request, @Body(ValidationPipe) stockOrderRequest: StockOrderRequestDto, ) { - await this.stockOrderService.buy(request.user.id, stockOrderRequest); + await this.stockOrderService.buy( + parseInt(request.user.userId, 10), + stockOrderRequest, + ); } @Post('/sell') @@ -54,13 +57,16 @@ export class StockOrderController { description: '주식 매도 예약 등록 성공', }) async sell( - @Req() request: RequestInterface, + @Req() request: Request, @Body(ValidationPipe) stockOrderRequest: StockOrderRequestDto, ) { - await this.stockOrderService.sell(request.user.id, stockOrderRequest); + await this.stockOrderService.sell( + parseInt(request.user.userId, 10), + stockOrderRequest, + ); } - @Delete('/:order_id') + @Delete('/:orderId') @ApiBearerAuth() @UseGuards(JwtAuthGuard) @ApiOperation({ @@ -71,10 +77,10 @@ export class StockOrderController { status: 200, description: '주식 매도/매수 취소 성공', }) - async cancel( - @Req() request: RequestInterface, - @Param('order_id') orderId: number, - ) { - await this.stockOrderService.cancel(request.user.id, orderId); + async cancel(@Req() request: Request, @Param('orderId') orderId: number) { + await this.stockOrderService.cancel( + parseInt(request.user.userId, 10), + orderId, + ); } } From e81574ebc2b985d2bef01acce6cf7e230fb24dec Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:17:46 +0900 Subject: [PATCH 06/81] =?UTF-8?q?=E2=9E=95=20add:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order-socket.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts index 6e2bec3b..dada0cfd 100644 --- a/BE/src/stock/order/stock-order-socket.service.ts +++ b/BE/src/stock/order/stock-order-socket.service.ts @@ -36,8 +36,8 @@ export class StockOrderSocketService { this.checkExecutableOrder( data[0], // 주식 코드 data[2], // 주식 체결가 - ).catch(() => { - throw new InternalServerErrorException(); + ).catch((err) => { + throw new InternalServerErrorException(err); }); }, ); From e0c8c27fe4c2b8f7166a164bbef332e8611b212d Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:23:37 +0900 Subject: [PATCH 07/81] =?UTF-8?q?=E2=9E=95=20add:=20=EB=A7=A4=EC=88=98?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=80=EC=9A=A9=20=EC=9E=90=EC=82=B0=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index bdbfe8ce..d97e48eb 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -11,6 +11,7 @@ import { TradeType } from './enum/trade-type'; import { StatusType } from './enum/status-type'; import { StockOrderSocketService } from './stock-order-socket.service'; import { UserStockRepository } from '../../userStock/user-stock.repository'; +import { AssetRepository } from '../../asset/asset.repository'; @Injectable() export class StockOrderService { @@ -18,9 +19,18 @@ export class StockOrderService { private readonly stockOrderRepository: StockOrderRepository, private readonly stockOrderSocketService: StockOrderSocketService, private readonly userStockRepository: UserStockRepository, + private readonly assetRepository: AssetRepository, ) {} async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { + const asset = await this.assetRepository.findOneBy({ user_id: userId }); + + if ( + asset && + asset.cash_balance >= stockOrderRequest.amount * stockOrderRequest.price + ) + throw new BadRequestException('가용 자산이 충분하지 않습니다.'); + const order = this.stockOrderRepository.create({ user_id: userId, stock_code: stockOrderRequest.stock_code, From 3b7999da907ed124ec939c0f31a9c26949cf7e91 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:27:54 +0900 Subject: [PATCH 08/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4=EC=88=98?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=80=EC=9A=A9=20=EC=9E=90=EC=82=B0=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index d97e48eb..1c13617e 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -27,7 +27,7 @@ export class StockOrderService { if ( asset && - asset.cash_balance >= stockOrderRequest.amount * stockOrderRequest.price + asset.cash_balance < stockOrderRequest.amount * stockOrderRequest.price ) throw new BadRequestException('가용 자산이 충분하지 않습니다.'); From 8ae299c6b303f774161798e5a7199e3817064443 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 10:38:59 +0900 Subject: [PATCH 09/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4=EC=88=98?= =?UTF-8?q?=EC=8B=9C=20=EB=B3=B4=EC=9C=A0=20=EC=A3=BC=EC=8B=9D=20=ED=8F=89?= =?UTF-8?q?=EA=B7=A0=EA=B0=80=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.repository.ts | 10 ++-------- BE/src/stock/order/stock-order.service.ts | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 2b419efa..94bfd37f 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -45,22 +45,16 @@ export class StockOrderRepository extends Repository { .execute(); await queryRunner.query( - ` - INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) - VALUES (?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - quantity = quantity + ?, - avg_price = (avg_price * quantity + ? * ?) / (quantity + ?) -`, + `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.price, order.amount, ], ); diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 1c13617e..dd6df0cc 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -50,7 +50,7 @@ export class StockOrderService { stock_code: stockOrderRequest.stock_code, }); - if (!userStock || userStock.quantity === 0) + if (!userStock || userStock.quantity < stockOrderRequest.amount) throw new BadRequestException('주식을 매도 수만큼 가지고 있지 않습니다.'); const order = this.stockOrderRepository.create({ From e454dc2cf2308891f3d44c58c139a6a4bb4766d0 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 15 Nov 2024 11:40:42 +0900 Subject: [PATCH 10/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=B3=B4=EC=9C=A0?= =?UTF-8?q?=20=EC=A3=BC=EC=8B=9D=20=ED=8F=89=EA=B7=A0=EA=B0=80=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/userStock/user-stock.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/userStock/user-stock.entity.ts b/BE/src/userStock/user-stock.entity.ts index a02bf7f3..5d0803dc 100644 --- a/BE/src/userStock/user-stock.entity.ts +++ b/BE/src/userStock/user-stock.entity.ts @@ -21,7 +21,7 @@ export class UserStock { @Column({ nullable: false }) quantity: number; - @Column('decimal', { nullable: false, precision: 10, scale: 5 }) + @Column('float', { nullable: false, scale: 5 }) avg_price: number; @UpdateDateColumn() From d68955bb80167b25878ce6b73ee1f8829dd7c347 Mon Sep 17 00:00:00 2001 From: JIN Date: Fri, 15 Nov 2024 13:52:20 +0900 Subject: [PATCH 11/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B2=B4=EA=B2=B0=EA=B0=80=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=97=B0=EA=B2=B0=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20test.html=20=EC=83=9D=EC=84=B1#56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 1 + .../stock-trade-history-socket.service.ts | 34 ++-- BE/src/stock/trade/history/test.html | 181 ++++++++++++++++++ 3 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 BE/src/stock/trade/history/test.html diff --git a/BE/src/main.ts b/BE/src/main.ts index ed080e3c..97508254 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -15,6 +15,7 @@ async function bootstrap() { 'http://juga.kro.kr:5173', 'http://juga.kro.kr:3000', 'http://223.130.151.42:3000', + 'http://localhost:63342', ], methods: 'GET, HEAD, PUT, PATH, POST, DELETE', preflightContinue: false, 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 index d97a818b..c96371b7 100644 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts @@ -2,10 +2,18 @@ import { Injectable, Logger } from '@nestjs/common'; import { SocketGateway } from '../../../websocket/socket.gateway'; import { BaseSocketService } from '../../../websocket/base-socket.service'; +interface TradeHistoryData { + stck_prpr: string; // 체결가(주식 현재가) + cntg_vol: string; // 체결 거래량 + prdy_ctrt: string; // 전일 대비율 + stck_cntg_hour: string; // 주식 체결 시간 +} + @Injectable() export class StockTradeHistorySocketService { private TR_ID = 'H0STCNT0'; private readonly logger = new Logger('StockTradeHistorySocketService'); + private subscribedStocks = new Set(); constructor( private readonly socketGateway: SocketGateway, @@ -13,21 +21,26 @@ export class StockTradeHistorySocketService { ) { baseSocketService.registerSocketOpenHandler(() => { this.logger.debug('trade-history 소켓 연결 성공'); + this.subscribedStocks.forEach((stockCode) => { + this.baseSocketService.registerCode(this.TR_ID, stockCode); + }); }); baseSocketService.registerSocketDataHandler( this.TR_ID, (dataList: string[]) => { try { - // BaseSocketService에서 이미 split된 데이터가 옴 - const tradeData = { + const stockCode = dataList[0]; + + const tradeData: TradeHistoryData = { stck_prpr: dataList[2], cntg_vol: dataList[12], prdy_ctrt: dataList[5], stck_cntg_hour: dataList[1], }; - const eventName = `trade-history/${dataList[0]}`; + const eventName = `trade-history/${stockCode}`; + this.logger.debug(`Emitting trade data for ${stockCode}`); this.socketGateway.sendStockTradeHistoryValueToClient( eventName, tradeData, @@ -41,13 +54,12 @@ export class StockTradeHistorySocketService { } subscribeByCode(stockCode: string) { - try { - this.logger.log(`Subscribing to stock: ${stockCode}`); - this.baseSocketService.registerCode(this.TR_ID, stockCode); - this.logger.log(`Successfully subscribed to stock: ${stockCode}`); - } catch (error) { - this.logger.error(`Failed to subscribe to stock ${stockCode}:`, error); - throw error; - } + this.baseSocketService.registerCode(this.TR_ID, stockCode); + this.subscribedStocks.add(stockCode); + } + + unsubscribeByCode(stockCode: string) { + this.baseSocketService.unregisterCode(this.TR_ID, stockCode); + this.subscribedStocks.delete(stockCode); } } diff --git a/BE/src/stock/trade/history/test.html b/BE/src/stock/trade/history/test.html new file mode 100644 index 00000000..0aecd1a1 --- /dev/null +++ b/BE/src/stock/trade/history/test.html @@ -0,0 +1,181 @@ + + + + + 주식 실시간 데이터 테스트 + + + + +
+

주식 실시간 데이터 테스트

+ +
+ +
+ + +
+
+ +
+

실시간 데이터가 여기에 표시됩니다...

+
+
+ + + + \ No newline at end of file From 43f5c1e6f9469bdc9b2687ede1a68e01b6e2577e Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 18 Nov 2024 13:35:26 +0900 Subject: [PATCH 12/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20alpha=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20CD/CD=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index db8ea854..36e91731 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -17,10 +17,7 @@ jobs: max-parallel: 1 matrix: app: - [ - { name: 'be', dir: 'BE', port: 3000, container: 'juga-docker-be' }, - { name: 'fe', dir: 'FE', port: 5173, container: 'juga-docker-fe' }, - ] + [{ name: 'be', dir: 'BE', port: 3000, container: 'juga-docker-be' }] steps: - uses: actions/checkout@v4 @@ -35,11 +32,7 @@ jobs: - name: Create .env file run: | touch ./${{ matrix.app.dir }}/.env - if [ "${{ matrix.app.name }}" = "be" ]; then - echo "${{ secrets.ENV }}" > ./${{matrix.app.dir}}/.env - else - echo "${{ secrets.ENV_FE }}" > ./${{matrix.app.dir}}/.env - fi + echo "${{ secrets.ENV }}" > ./${{matrix.app.dir}}/.env - name: Install dependencies working-directory: ./${{matrix.app.dir}} @@ -108,12 +101,7 @@ jobs: port: 22 script: | docker system prune -af - if [ "${{ matrix.app.name }}" = "be" ]; then - echo "${{ secrets.ENV }}" > .env - else - echo "${{ secrets.ENV_FE }}" > .env - fi - + echo "${{ secrets.ENV }}" > .env docker network create juga-network || true From f8b6560a05626816e573101f1918aa564606f5f4 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 18 Nov 2024 13:36:03 +0900 Subject: [PATCH 13/81] =?UTF-8?q?=F0=9F=9A=9A=20rename=20:alpha=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/{deply-alpha.yml => deploy-alpha.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{deply-alpha.yml => deploy-alpha.yml} (100%) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deploy-alpha.yml similarity index 100% rename from .github/workflows/deply-alpha.yml rename to .github/workflows/deploy-alpha.yml From 0ed0871209e8b38a21ac8352da211c4976b64fcd Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 18 Nov 2024 13:56:22 +0900 Subject: [PATCH 14/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B2=B4=EA=B2=B0=EA=B0=80=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=97=B0=EA=B2=B0=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20test.html=20=EC=83=9D=EC=84=B1#127?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock-trade-history-socket.service.ts | 143 +++++++++++++----- 1 file changed, 104 insertions(+), 39 deletions(-) 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 index c96371b7..f7c3e92b 100644 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts @@ -1,6 +1,12 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { + Injectable, + Logger, + OnModuleInit, +} from '@nestjs/common'; import { SocketGateway } from '../../../websocket/socket.gateway'; -import { BaseSocketService } from '../../../websocket/base-socket.service'; +import { WebSocket } from 'ws'; +import axios from 'axios'; +import { SocketConnectTokenInterface } from '../../../websocket/interface/socket.interface'; interface TradeHistoryData { stck_prpr: string; // 체결가(주식 현재가) @@ -10,56 +16,115 @@ interface TradeHistoryData { } @Injectable() -export class StockTradeHistorySocketService { +export class StockTradeHistorySocketService implements OnModuleInit { private TR_ID = 'H0STCNT0'; private readonly logger = new Logger('StockTradeHistorySocketService'); private subscribedStocks = new Set(); + private socket: WebSocket; + private socketConnectionKey: string; - constructor( - private readonly socketGateway: SocketGateway, - private readonly baseSocketService: BaseSocketService, - ) { - baseSocketService.registerSocketOpenHandler(() => { - this.logger.debug('trade-history 소켓 연결 성공'); - this.subscribedStocks.forEach((stockCode) => { - this.baseSocketService.registerCode(this.TR_ID, stockCode); - }); - }); - - baseSocketService.registerSocketDataHandler( - this.TR_ID, - (dataList: string[]) => { - try { - const stockCode = dataList[0]; - - const tradeData: TradeHistoryData = { - stck_prpr: dataList[2], - cntg_vol: dataList[12], - prdy_ctrt: dataList[5], - stck_cntg_hour: dataList[1], - }; - - const eventName = `trade-history/${stockCode}`; - this.logger.debug(`Emitting trade data for ${stockCode}`); - this.socketGateway.sendStockTradeHistoryValueToClient( - eventName, - tradeData, + 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.registerCode(this.TR_ID, '005930'); + }; + + 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, ); - } catch (error) { - this.logger.error('Error processing trade data:', error); - this.logger.error('Raw data was:', dataList); - } - }, + if (json.header.tr_id === 'PINGPONG') + this.socket.pong(JSON.stringify(json)); + return; + } + + const dataList = data[3].split('^'); + + if (Number(dataList[1]) % 500 === 0) + this.logger.log(`한국투자증권 데이터 수신 성공 (5분 단위)`, data[1]); + + console.log(dataList); + }; + + this.socket.onclose = () => { + this.logger.warn(`한국투자증권 소켓 연결 종료`); + }; + } + + 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, + }, + }, + }), ); } subscribeByCode(stockCode: string) { - this.baseSocketService.registerCode(this.TR_ID, stockCode); this.subscribedStocks.add(stockCode); } unsubscribeByCode(stockCode: string) { - this.baseSocketService.unregisterCode(this.TR_ID, stockCode); this.subscribedStocks.delete(stockCode); } + + async getSocketConnectionKey() { + if (this.socketConnectionKey) { + return this.socketConnectionKey; + } + + const response = await axios.post( + 'https://openapivts.koreainvestment.com:29443/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; + } } From e15169bc6359eea221b849b1b5026c2cb718e28e Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 18 Nov 2024 13:58:30 +0900 Subject: [PATCH 15/81] =?UTF-8?q?=F0=9F=94=A5=20remove:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20html=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C#127?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/trade/history/test.html | 181 --------------------------- 1 file changed, 181 deletions(-) delete mode 100644 BE/src/stock/trade/history/test.html diff --git a/BE/src/stock/trade/history/test.html b/BE/src/stock/trade/history/test.html deleted file mode 100644 index 0aecd1a1..00000000 --- a/BE/src/stock/trade/history/test.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - 주식 실시간 데이터 테스트 - - - - -
-

주식 실시간 데이터 테스트

- -
- -
- - -
-
- -
-

실시간 데이터가 여기에 표시됩니다...

-
-
- - - - \ No newline at end of file From 279bd4b27882968829bf86f9118957639f837332 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 18 Nov 2024 15:53:14 +0900 Subject: [PATCH 16/81] =?UTF-8?q?=F0=9F=93=9D=20docs:=20swagger=EC=97=90?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/trade/history/stock-trade-history.controller.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 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 8332b51b..54a67b41 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 { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; -import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-history-response.dto'; -import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; 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'; @ApiTags('주식현재가 체결 조회 API') @Controller('/api/stocks/trade-history') @@ -25,10 +25,9 @@ export class StockTradeHistoryController { @ApiResponse({ status: 200, description: '단일 주식 종목에 대한 주식현재가 체결값 조회 성공', - type: TodayStockTradeHistoryResponseDto, + type: TodayStockTradeHistoryDataDto, }) getTodayStockTradeHistory(@Param('stockCode') stockCode: string) { - this.stockTradeHistorySocketService.subscribeByCode(stockCode); return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } From 03ac949c44a004c7e360f6c2405203b9a4ee3f33 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 18 Nov 2024 15:54:03 +0900 Subject: [PATCH 17/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=AA=A8=EC=9D=98=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9A=A9=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80#127?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/util/get-full-URL.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BE/src/util/get-full-URL.ts b/BE/src/util/get-full-URL.ts index da79427f..7be7ea50 100644 --- a/BE/src/util/get-full-URL.ts +++ b/BE/src/util/get-full-URL.ts @@ -1,3 +1,7 @@ export const getFullURL = (url: string) => { return `${process.env.KOREA_INVESTMENT_BASE_URL}${url}`; }; + +export const getFullTestURL = (url: string) => { + return `${process.env.KOREA_INVESTMENT_TEST_BASE_URL}${url}`; +}; From 5212038fb29e39d9e2fcac4517d57a9edadbd1fb Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 18 Nov 2024 16:16:06 +0900 Subject: [PATCH 18/81] =?UTF-8?q?=E2=9C=A8=20feat:=20SSE=EB=A1=9C=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1#12?= =?UTF-8?q?7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/history/interface/sse-event.ts | 6 ++ .../stock-trade-history-socket.service.ts | 73 ++++++++++--------- .../history/stock-trade-history.controller.ts | 34 ++++++++- 3 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 BE/src/stock/trade/history/interface/sse-event.ts diff --git a/BE/src/stock/trade/history/interface/sse-event.ts b/BE/src/stock/trade/history/interface/sse-event.ts new file mode 100644 index 00000000..e20f58cb --- /dev/null +++ b/BE/src/stock/trade/history/interface/sse-event.ts @@ -0,0 +1,6 @@ +export interface SseEvent { + data: string; + id?: string; + type?: string; + retry?: number; +} 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 index f7c3e92b..4489d613 100644 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts @@ -1,37 +1,26 @@ -import { - Injectable, - Logger, - OnModuleInit, -} from '@nestjs/common'; -import { SocketGateway } from '../../../websocket/socket.gateway'; import { WebSocket } from 'ws'; import axios from 'axios'; +import { Observable, Subject } from 'rxjs'; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { SseEvent } from './interface/sse-event'; import { SocketConnectTokenInterface } from '../../../websocket/interface/socket.interface'; - -interface TradeHistoryData { - stck_prpr: string; // 체결가(주식 현재가) - cntg_vol: string; // 체결 거래량 - prdy_ctrt: string; // 전일 대비율 - stck_cntg_hour: string; // 주식 체결 시간 -} +import { getFullTestURL } from '../../../util/get-full-URL'; +import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; @Injectable() export class StockTradeHistorySocketService implements OnModuleInit { - private TR_ID = 'H0STCNT0'; - private readonly logger = new Logger('StockTradeHistorySocketService'); - private subscribedStocks = new Set(); + private readonly logger = new Logger(''); private socket: WebSocket; private socketConnectionKey: string; - - constructor(private readonly socketGateway: SocketGateway) {} + private subscribedStocks = new Set(); + private TR_ID = 'H0STCNT0'; + private eventSubject = new Subject(); async onModuleInit() { this.socketConnectionKey = await this.getSocketConnectionKey(); this.socket = new WebSocket(process.env.KOREA_INVESTMENT_TEST_SOCKET_URL); - this.socket.onopen = () => { - this.registerCode(this.TR_ID, '005930'); - }; + this.socket.onopen = () => {}; this.socket.onmessage = (event) => { const data = @@ -53,10 +42,20 @@ export class StockTradeHistorySocketService implements OnModuleInit { const dataList = data[3].split('^'); - if (Number(dataList[1]) % 500 === 0) - this.logger.log(`한국투자증권 데이터 수신 성공 (5분 단위)`, data[1]); - - console.log(dataList); + const tradeData: TodayStockTradeHistoryDataDto = { + stck_cntg_hour: dataList[1], + stck_prpr: dataList[2], + prdy_vrss_sign: dataList[3], + cntg_vol: dataList[12], + prdy_ctrt: dataList[5], + }; + + this.eventSubject.next({ + data: JSON.stringify({ + stockCode: data[1], + tradeData, + }), + }); }; this.socket.onclose = () => { @@ -64,6 +63,20 @@ export class StockTradeHistorySocketService implements OnModuleInit { }; } + getTradeDataStream(): Observable { + return this.eventSubject.asObservable(); + } + + 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({ @@ -102,21 +115,13 @@ export class StockTradeHistorySocketService implements OnModuleInit { ); } - subscribeByCode(stockCode: string) { - this.subscribedStocks.add(stockCode); - } - - unsubscribeByCode(stockCode: string) { - this.subscribedStocks.delete(stockCode); - } - async getSocketConnectionKey() { if (this.socketConnectionKey) { return this.socketConnectionKey; } const response = await axios.post( - 'https://openapivts.koreainvestment.com:29443/oauth2/Approval', + getFullTestURL('/oauth2/Approval'), { grant_type: 'client_credentials', appkey: process.env.KOREA_INVESTMENT_TEST_APP_KEY, 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 54a67b41..cbbe09c1 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,11 @@ -import { Controller, Get, Param } from '@nestjs/common'; +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 { 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') @@ -48,4 +50,34 @@ export class StockTradeHistoryController { getDailyStockTradeHistory(@Param('stockCode') stockCode: string) { 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() + .subscribe(subscriber); + + return () => { + this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); + subscription.unsubscribe(); + }; + }); + } } From e6cf90a6727f0eddd1de18ee9494a0a4f7c1cd4f Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 18 Nov 2024 16:39:47 +0900 Subject: [PATCH 19/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20cors=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=84=A4=EC=A0=95=EC=97=90=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=86=8C=EA=B0=80=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=EB=90=98=EC=96=B4=EC=9E=88=EB=8D=98=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95#127?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/BE/src/main.ts b/BE/src/main.ts index 97508254..ed080e3c 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -15,7 +15,6 @@ async function bootstrap() { 'http://juga.kro.kr:5173', 'http://juga.kro.kr:3000', 'http://223.130.151.42:3000', - 'http://localhost:63342', ], methods: 'GET, HEAD, PUT, PATH, POST, DELETE', preflightContinue: false, From 9a4eaf2224012c32754e179ee13d3e86631b42ea Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 18 Nov 2024 16:54:46 +0900 Subject: [PATCH 20/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=86=8C=EC=BC=93?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EC=A2=85=EB=A3=8C=20=EC=8B=9C=201?= =?UTF-8?q?=EB=B6=84=20=EA=B0=84=EA=B2=A9=EC=9C=BC=EB=A1=9C=20=EC=9E=AC?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#126?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/base-socket.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 66bc6a1d..d075fffe 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -63,6 +63,11 @@ export class BaseSocketService implements OnModuleInit { this.socket.onclose = () => { this.logger.warn(`한국투자증권 소켓 연결 종료`); + setTimeout(() => { + this.onModuleInit().catch((err) => { + throw new InternalServerErrorException(err); + }); + }, 60000); }; } From 0f3e141c94380f231532e7dc2f570d5890c4db84 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 18 Nov 2024 17:45:28 +0900 Subject: [PATCH 21/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20alpha,=20product?= =?UTF-8?q?ion=20ENV=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-alpha.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-alpha.yml b/.github/workflows/deploy-alpha.yml index 36e91731..ba52ca0e 100644 --- a/.github/workflows/deploy-alpha.yml +++ b/.github/workflows/deploy-alpha.yml @@ -32,7 +32,7 @@ jobs: - name: Create .env file run: | touch ./${{ matrix.app.dir }}/.env - echo "${{ secrets.ENV }}" > ./${{matrix.app.dir}}/.env + echo "${{ secrets.ENV_ALPHA }}" > ./${{matrix.app.dir}}/.env - name: Install dependencies working-directory: ./${{matrix.app.dir}} @@ -101,7 +101,7 @@ jobs: port: 22 script: | docker system prune -af - echo "${{ secrets.ENV }}" > .env + echo "${{ secrets.ENV_ALPHA }}" > .env docker network create juga-network || true From 4df492aba87d5a67e3fe16e8a4ccd4ef11334370 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 18 Nov 2024 18:19:46 +0900 Subject: [PATCH 22/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20requestA?= =?UTF-8?q?pi=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../korea-investment.service.ts | 42 +++++- BE/src/stock/detail/stock-detail.service.ts | 62 ++------ BE/src/stock/index/stock-index.controller.ts | 64 ++------ BE/src/stock/index/stock-index.service.ts | 138 ++++++++---------- BE/src/stock/topfive/stock-topfive.service.ts | 61 +++----- .../history/stock-trade-history.service.ts | 62 ++------ 6 files changed, 161 insertions(+), 268 deletions(-) diff --git a/BE/src/koreaInvestment/korea-investment.service.ts b/BE/src/koreaInvestment/korea-investment.service.ts index 29dc323c..43eb43e2 100644 --- a/BE/src/koreaInvestment/korea-investment.service.ts +++ b/BE/src/koreaInvestment/korea-investment.service.ts @@ -1,12 +1,15 @@ import axios from 'axios'; -import { UnauthorizedException } from '@nestjs/common'; +import { Logger, UnauthorizedException } from '@nestjs/common'; import { getFullURL } from '../util/get-full-URL'; import { AccessTokenInterface } from './interface/korea-investment.interface'; +import { getHeader } from '../util/get-header'; export class KoreaInvestmentService { private accessToken: string; private tokenExpireTime: Date; + private readonly logger = new Logger(); + async getAccessToken() { // accessToken이 유효한 경우 if (this.accessToken && this.tokenExpireTime > new Date()) { @@ -29,4 +32,41 @@ export class KoreaInvestmentService { return this.accessToken; } + + /** + * @private 한국투자 Open API - API 호출용 공통 함수 + * @param {string} trId - API 호출에 사용할 tr_id + * @param {string} apiURL - API 호출에 사용할 URL + * @param {Record} params - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - API 호출에 대한 응답 데이터 + * + * @author uuuo3o + */ + async requestApi( + trId: string, + apiURL: string, + params: Record, + ): Promise { + try { + const accessToken = await this.getAccessToken(); + const headers = getHeader(accessToken, trId); + const url = getFullURL(apiURL); + + 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; + } + } } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 28589a67..8ae002ff 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -1,8 +1,5 @@ -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 { InquirePriceChartApiResponse } from './interface/stock-detail-chart.interface'; import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; import { @@ -35,11 +32,12 @@ export class StockDetailService { fid_input_iscd: stockCode, }; - const response = await this.requestApi( - 'FHKST01010100', - '/uapi/domestic-stock/v1/quotations/inquire-price', - queryParams, - ); + const response = + await this.koreaInvestmentService.requestApi( + 'FHKST01010100', + '/uapi/domestic-stock/v1/quotations/inquire-price', + queryParams, + ); return await this.formatStockData(response.output); } catch (error) { @@ -106,11 +104,12 @@ export class StockDetailService { fid_org_adj_prc: '0', }; - const response = await this.requestApi( - 'FHKST03010100', - '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', - queryParams, - ); + const response = + await this.koreaInvestmentService.requestApi( + 'FHKST03010100', + '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', + queryParams, + ); return this.formatStockInquirePriceData(response).slice().reverse(); } catch (error) { @@ -158,41 +157,4 @@ export class StockDetailService { return stockData; }); } - - /** - * @private 한국투자 Open API - API 호출용 공통 함수 - * @param {string} trId - API 호출에 사용할 tr_id - * @param {string} apiURL - API 호출에 사용할 URL - * @param {Record} params - API 요청 시 필요한 쿼리 파라미터 DTO - * @returns - API 호출에 대한 응답 데이터 - * - * @author uuuo3o - */ - private async requestApi( - trId: string, - apiURL: string, - params: Record, - ): Promise { - try { - const accessToken = await this.koreaInvestmentService.getAccessToken(); - const headers = getHeader(accessToken, trId); - const url = getFullURL(apiURL); - - 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; - } - } } diff --git a/BE/src/stock/index/stock-index.controller.ts b/BE/src/stock/index/stock-index.controller.ts index 330685e2..9ac6f114 100644 --- a/BE/src/stock/index/stock-index.controller.ts +++ b/BE/src/stock/index/stock-index.controller.ts @@ -26,46 +26,22 @@ export class StockIndexController { type: StockIndexResponseDto, }) async getStockIndex() { - const accessToken = await this.koreaInvestmentService.getAccessToken(); + 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 + this.stockIndexService.getDomesticStockIndexListByCode('0001'), // 코스피 + this.stockIndexService.getDomesticStockIndexListByCode('1001'), // 코스닥 + this.stockIndexService.getDomesticStockIndexListByCode('2001'), // 코스피200 + this.stockIndexService.getDomesticStockIndexListByCode('3003'), // 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 + this.stockIndexService.getDomesticStockIndexValueByCode('0001'), // 코스피 + this.stockIndexService.getDomesticStockIndexValueByCode('1001'), // 코스닥 + this.stockIndexService.getDomesticStockIndexValueByCode('2001'), // 코스피200 + this.stockIndexService.getDomesticStockIndexValueByCode('3003'), // KSQ150 ]); const stockIndexResponse = new StockIndexResponseDto(); @@ -90,25 +66,13 @@ export class StockIndexController { @Cron('*/5 9-16 * * 1-5') async cronStockIndexLists() { - const accessToken = await this.koreaInvestmentService.getAccessToken(); + 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.stockIndexService.getDomesticStockIndexListByCode('0001'), // 코스피 + this.stockIndexService.getDomesticStockIndexListByCode('1001'), // 코스닥 + this.stockIndexService.getDomesticStockIndexListByCode('2001'), // 코스피200 + this.stockIndexService.getDomesticStockIndexListByCode('3003'), // KSQ150 ]); this.socketGateway.sendStockIndexListToClient({ diff --git a/BE/src/stock/index/stock-index.service.ts b/BE/src/stock/index/stock-index.service.ts index fa05ed4c..d3146717 100644 --- a/BE/src/stock/index/stock-index.service.ts +++ b/BE/src/stock/index/stock-index.service.ts @@ -1,95 +1,85 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import axios from 'axios'; +import { Injectable, Logger } from '@nestjs/common'; 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'; +import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; @Injectable() export class StockIndexService { - async getDomesticStockIndexListByCode(code: string, accessToken: string) { - const result = await this.requestDomesticStockIndexListApi( - code, - accessToken, - ); + private readonly logger = new Logger(); - return result.output.map((element) => { - return new StockIndexListChartElementDto( - element.bsop_hour, - element.bstp_nmix_prpr, - element.bstp_nmix_prdy_vrss, - ); - }); - } + constructor( + private readonly koreaInvestmentService: KoreaInvestmentService, + ) {} - async getDomesticStockIndexValueByCode(code: string, accessToken: string) { - const result = await this.requestDomesticStockIndexValueApi( - code, - accessToken, - ); + async getDomesticStockIndexListByCode(code: string) { + try { + const queryParams = { + fid_input_hour_1: '300', + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }; - 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( + const result = + await this.koreaInvestmentService.requestApi( + 'FHPUP02110200', '/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( - '주가 지수 차트 정보를 조회하지 못했습니다.', + queryParams, ); - }); - return response.data; + return result.output.map((element) => { + return new StockIndexListChartElementDto( + element.bsop_hour, + element.bstp_nmix_prpr, + element.bstp_nmix_prdy_vrss, + ); + }); + } 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 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( - '주가 지수 값 정보를 조회하지 못했습니다.', + async getDomesticStockIndexValueByCode(code: string) { + try { + const queryParams = { + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }; + + const result = + await this.koreaInvestmentService.requestApi( + 'FHPUP02100000', + '/uapi/domestic-stock/v1/quotations/inquire-index-price', + queryParams, ); - }); - return response.data; + 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, + ); + } 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; + } } } diff --git a/BE/src/stock/topfive/stock-topfive.service.ts b/BE/src/stock/topfive/stock-topfive.service.ts index 82f659d5..16489241 100644 --- a/BE/src/stock/topfive/stock-topfive.service.ts +++ b/BE/src/stock/topfive/stock-topfive.service.ts @@ -1,4 +1,3 @@ -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'; @@ -8,8 +7,6 @@ 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() @@ -20,38 +17,6 @@ export class StockTopfiveService { 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) @@ -81,15 +46,25 @@ export class StockTopfiveService { break; } - const highResponse = await this.requestApi({ - ...queryParams, - fid_rank_sort_cls_code: '0', - }); + const highResponse = + await this.koreaInvestmentService.requestApi( + 'FHPST01700000', + '/uapi/domestic-stock/v1/ranking/fluctuation', + this.getStockRankingParams({ + ...queryParams, + fid_rank_sort_cls_code: '0', + }), + ); - const lowResponse = await this.requestApi({ - ...queryParams, - fid_rank_sort_cls_code: '1', - }); + const lowResponse = + await this.koreaInvestmentService.requestApi( + 'FHPST01700000', + '/uapi/domestic-stock/v1/ranking/fluctuation', + this.getStockRankingParams({ + ...queryParams, + fid_rank_sort_cls_code: '1', + }), + ); const response = new StockRankingResponseDto(); response.high = this.formatStockData(highResponse.output); 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 449b9451..2b53bd3d 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -1,8 +1,5 @@ -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 { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; import { TodayStockTradeHistoryOutputDto } from './dto/today-stock-trade-history-output.dto'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; @@ -32,11 +29,12 @@ export class StockTradeHistoryService { fid_input_iscd: stockCode, }; - const response = await this.requestApi( - 'FHKST01010300', - '/uapi/domestic-stock/v1/quotations/inquire-ccnl', - queryParams, - ); + const response = + await this.koreaInvestmentService.requestApi( + 'FHKST01010300', + '/uapi/domestic-stock/v1/quotations/inquire-ccnl', + queryParams, + ); return this.formatTodayStockTradeHistoryData(response.output); } catch (error) { @@ -89,11 +87,12 @@ export class StockTradeHistoryService { fid_org_adj_prc: '0', }; - const response = await this.requestApi( - 'FHKST01010400', - '/uapi/domestic-stock/v1/quotations/inquire-daily-price', - queryParams, - ); + const response = + await this.koreaInvestmentService.requestApi( + 'FHKST01010400', + '/uapi/domestic-stock/v1/quotations/inquire-daily-price', + queryParams, + ); return this.formatDailyStockTradeHistoryData(response.output); } catch (error) { @@ -132,41 +131,4 @@ export class StockTradeHistoryService { return historyData; }); } - - /** - * @private 한국투자 Open API - API 호출용 공통 함수 - * @param {string} trId - API 호출에 사용할 tr_id - * @param {string} apiURL - API 호출에 사용할 URL - * @param {Record} params - API 요청 시 필요한 쿼리 파라미터 DTO - * @returns - API 호출에 대한 응답 데이터 - * - * @author uuuo3o - */ - private async requestApi( - trId: string, - apiURL: string, - params: Record, - ): Promise { - try { - const accessToken = await this.koreaInvestmentService.getAccessToken(); - const headers = getHeader(accessToken, trId); - const url = getFullURL(apiURL); - - 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; - } - } } From b6ec95cf16abb6d9be30b7938ea5091b663b0b3a Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 18 Nov 2024 18:30:06 +0900 Subject: [PATCH 23/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A5=BC=20domain-service=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 4 ++-- .../interface/korea-investment.interface.ts | 0 .../korea-investment.domain-service.ts} | 6 +++--- .../koreaInvestment/korea-investment.module.ts | 10 ++++++++++ .../websocket/base-socket.domain-service.ts} | 10 ++++++---- .../websocket/interface/socket.interface.ts | 0 .../websocket/socket-token.domain-service.ts} | 4 ++-- .../{ => common}/websocket/socket.gateway.ts | 0 BE/src/common/websocket/socket.module.ts | 10 ++++++++++ .../koreaInvestment/korea-investment.module.ts | 10 ---------- BE/src/stock/detail/stock-detail.module.ts | 2 +- BE/src/stock/detail/stock-detail.service.ts | 8 ++++---- .../stock/index/stock-index-socket.service.ts | 18 +++++++++--------- BE/src/stock/index/stock-index.controller.ts | 10 +++++----- BE/src/stock/index/stock-index.module.ts | 4 ++-- BE/src/stock/index/stock-index.service.ts | 8 ++++---- .../stock/order/stock-order-socket.service.ts | 16 ++++++++-------- BE/src/stock/order/stock-order.module.ts | 2 +- BE/src/stock/topfive/stock-topfive.module.ts | 2 +- BE/src/stock/topfive/stock-topfive.service.ts | 8 ++++---- .../history/stock-trade-history.module.ts | 2 +- .../history/stock-trade-history.service.ts | 8 ++++---- BE/src/websocket/socket.module.ts | 10 ---------- 23 files changed, 77 insertions(+), 75 deletions(-) rename BE/src/{ => common}/koreaInvestment/interface/korea-investment.interface.ts (100%) rename BE/src/{koreaInvestment/korea-investment.service.ts => common/koreaInvestment/korea-investment.domain-service.ts} (93%) create mode 100644 BE/src/common/koreaInvestment/korea-investment.module.ts rename BE/src/{websocket/base-socket.service.ts => common/websocket/base-socket.domain-service.ts} (90%) rename BE/src/{ => common}/websocket/interface/socket.interface.ts (100%) rename BE/src/{websocket/socket-token.service.ts => common/websocket/socket-token.domain-service.ts} (86%) rename BE/src/{ => common}/websocket/socket.gateway.ts (100%) create mode 100644 BE/src/common/websocket/socket.module.ts delete mode 100644 BE/src/koreaInvestment/korea-investment.module.ts delete mode 100644 BE/src/websocket/socket.module.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 2b2a14d5..39527728 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -8,8 +8,8 @@ import { AppService } from './app.service'; import { AuthModule } from './auth/auth.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'; +import { KoreaInvestmentModule } from './common/koreaInvestment/korea-investment.module'; +import { SocketModule } from './common/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'; diff --git a/BE/src/koreaInvestment/interface/korea-investment.interface.ts b/BE/src/common/koreaInvestment/interface/korea-investment.interface.ts similarity index 100% rename from BE/src/koreaInvestment/interface/korea-investment.interface.ts rename to BE/src/common/koreaInvestment/interface/korea-investment.interface.ts diff --git a/BE/src/koreaInvestment/korea-investment.service.ts b/BE/src/common/koreaInvestment/korea-investment.domain-service.ts similarity index 93% rename from BE/src/koreaInvestment/korea-investment.service.ts rename to BE/src/common/koreaInvestment/korea-investment.domain-service.ts index 43eb43e2..3e96c55b 100644 --- a/BE/src/koreaInvestment/korea-investment.service.ts +++ b/BE/src/common/koreaInvestment/korea-investment.domain-service.ts @@ -1,10 +1,10 @@ import axios from 'axios'; import { Logger, UnauthorizedException } from '@nestjs/common'; -import { getFullURL } from '../util/get-full-URL'; +import { getFullURL } from '../../util/get-full-URL'; import { AccessTokenInterface } from './interface/korea-investment.interface'; -import { getHeader } from '../util/get-header'; +import { getHeader } from '../../util/get-header'; -export class KoreaInvestmentService { +export class KoreaInvestmentDomainService { private accessToken: string; private tokenExpireTime: Date; diff --git a/BE/src/common/koreaInvestment/korea-investment.module.ts b/BE/src/common/koreaInvestment/korea-investment.module.ts new file mode 100644 index 00000000..2ffd0889 --- /dev/null +++ b/BE/src/common/koreaInvestment/korea-investment.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { KoreaInvestmentDomainService } from './korea-investment.domain-service'; + +@Module({ + imports: [], + controllers: [], + providers: [KoreaInvestmentDomainService], + exports: [KoreaInvestmentDomainService], +}) +export class KoreaInvestmentModule {} diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/common/websocket/base-socket.domain-service.ts similarity index 90% rename from BE/src/websocket/base-socket.service.ts rename to BE/src/common/websocket/base-socket.domain-service.ts index 66bc6a1d..3eb70b2c 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/common/websocket/base-socket.domain-service.ts @@ -5,10 +5,10 @@ import { Logger, OnModuleInit, } from '@nestjs/common'; -import { SocketTokenService } from './socket-token.service'; +import { SocketTokenDomainService } from './socket-token.domain-service'; @Injectable() -export class BaseSocketService implements OnModuleInit { +export class BaseSocketDomainService implements OnModuleInit { private socket: WebSocket; private socketConnectionKey: string; private socketOpenHandlers: (() => void | Promise)[] = []; @@ -18,11 +18,13 @@ export class BaseSocketService implements OnModuleInit { private readonly logger = new Logger(); - constructor(private readonly socketTokenService: SocketTokenService) {} + constructor( + private readonly socketTokenDomainService: SocketTokenDomainService, + ) {} async onModuleInit() { this.socketConnectionKey = - await this.socketTokenService.getSocketConnectionKey(); + await this.socketTokenDomainService.getSocketConnectionKey(); this.socket = new WebSocket(process.env.KOREA_INVESTMENT_SOCKET_URL); this.socket.onopen = () => { diff --git a/BE/src/websocket/interface/socket.interface.ts b/BE/src/common/websocket/interface/socket.interface.ts similarity index 100% rename from BE/src/websocket/interface/socket.interface.ts rename to BE/src/common/websocket/interface/socket.interface.ts diff --git a/BE/src/websocket/socket-token.service.ts b/BE/src/common/websocket/socket-token.domain-service.ts similarity index 86% rename from BE/src/websocket/socket-token.service.ts rename to BE/src/common/websocket/socket-token.domain-service.ts index f4ff9e3c..cce5ea03 100644 --- a/BE/src/websocket/socket-token.service.ts +++ b/BE/src/common/websocket/socket-token.domain-service.ts @@ -1,8 +1,8 @@ import axios from 'axios'; import { SocketConnectTokenInterface } from './interface/socket.interface'; -import { getFullURL } from '../util/get-full-URL'; +import { getFullURL } from '../../util/get-full-URL'; -export class SocketTokenService { +export class SocketTokenDomainService { private approvalKey: string; async getSocketConnectionKey() { diff --git a/BE/src/websocket/socket.gateway.ts b/BE/src/common/websocket/socket.gateway.ts similarity index 100% rename from BE/src/websocket/socket.gateway.ts rename to BE/src/common/websocket/socket.gateway.ts diff --git a/BE/src/common/websocket/socket.module.ts b/BE/src/common/websocket/socket.module.ts new file mode 100644 index 00000000..0cc8ceac --- /dev/null +++ b/BE/src/common/websocket/socket.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SocketGateway } from './socket.gateway'; +import { SocketTokenDomainService } from './socket-token.domain-service'; +import { BaseSocketDomainService } from './base-socket.domain-service'; + +@Module({ + providers: [SocketTokenDomainService, SocketGateway, BaseSocketDomainService], + exports: [SocketGateway, BaseSocketDomainService], +}) +export class SocketModule {} diff --git a/BE/src/koreaInvestment/korea-investment.module.ts b/BE/src/koreaInvestment/korea-investment.module.ts deleted file mode 100644 index 8e7dd10a..00000000 --- a/BE/src/koreaInvestment/korea-investment.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { KoreaInvestmentService } from './korea-investment.service'; - -@Module({ - imports: [], - controllers: [], - providers: [KoreaInvestmentService], - exports: [KoreaInvestmentService], -}) -export class KoreaInvestmentModule {} diff --git a/BE/src/stock/detail/stock-detail.module.ts b/BE/src/stock/detail/stock-detail.module.ts index b6447a76..50f25a5e 100644 --- a/BE/src/stock/detail/stock-detail.module.ts +++ b/BE/src/stock/detail/stock-detail.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; +import { KoreaInvestmentModule } from '../../common/koreaInvestment/korea-investment.module'; import { StockDetailController } from './stock-detail.controller'; import { StockDetailService } from './stock-detail.service'; import { StockDetailRepository } from './stock-detail.repository'; diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 8ae002ff..389d7147 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; +import { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea-investment.domain-service'; import { InquirePriceChartApiResponse } from './interface/stock-detail-chart.interface'; import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; import { @@ -14,7 +14,7 @@ export class StockDetailService { private readonly logger = new Logger(); constructor( - private readonly koreaInvestmentService: KoreaInvestmentService, + private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, private readonly stockDetailRepository: StockDetailRepository, ) {} @@ -33,7 +33,7 @@ export class StockDetailService { }; const response = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHKST01010100', '/uapi/domestic-stock/v1/quotations/inquire-price', queryParams, @@ -105,7 +105,7 @@ export class StockDetailService { }; const response = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHKST03010100', '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', queryParams, diff --git a/BE/src/stock/index/stock-index-socket.service.ts b/BE/src/stock/index/stock-index-socket.service.ts index f82ba9a2..05ecb307 100644 --- a/BE/src/stock/index/stock-index-socket.service.ts +++ b/BE/src/stock/index/stock-index-socket.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { StockIndexValueElementDto } from './dto/stock-index-value-element.dto'; -import { BaseSocketService } from '../../websocket/base-socket.service'; -import { SocketGateway } from '../../websocket/socket.gateway'; +import { BaseSocketDomainService } from '../../common/websocket/base-socket.domain-service'; +import { SocketGateway } from '../../common/websocket/socket.gateway'; @Injectable() export class StockIndexSocketService { @@ -15,16 +15,16 @@ export class StockIndexSocketService { constructor( private readonly socketGateway: SocketGateway, - private readonly baseSocketService: BaseSocketService, + private readonly baseSocketDomainService: BaseSocketDomainService, ) { - baseSocketService.registerSocketOpenHandler(() => { - this.baseSocketService.registerCode(this.TR_ID, '0001'); // 코스피 - this.baseSocketService.registerCode(this.TR_ID, '1001'); // 코스닥 - this.baseSocketService.registerCode(this.TR_ID, '2001'); // 코스피200 - this.baseSocketService.registerCode(this.TR_ID, '3003'); // KSQ150 + 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 }); - baseSocketService.registerSocketDataHandler( + baseSocketDomainService.registerSocketDataHandler( this.TR_ID, (data: string[]) => { this.socketGateway.sendStockIndexValueToClient( diff --git a/BE/src/stock/index/stock-index.controller.ts b/BE/src/stock/index/stock-index.controller.ts index 9ac6f114..09d5bd40 100644 --- a/BE/src/stock/index/stock-index.controller.ts +++ b/BE/src/stock/index/stock-index.controller.ts @@ -3,15 +3,15 @@ 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'; +import { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea-investment.domain-service'; +import { SocketGateway } from '../../common/websocket/socket.gateway'; @Controller('/api/stocks/index') @ApiTags('주가 지수 API') export class StockIndexController { constructor( private readonly stockIndexService: StockIndexService, - private readonly koreaInvestmentService: KoreaInvestmentService, + private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, private readonly socketGateway: SocketGateway, ) {} @@ -26,7 +26,7 @@ export class StockIndexController { type: StockIndexResponseDto, }) async getStockIndex() { - await this.koreaInvestmentService.getAccessToken(); + await this.koreaInvestmentDomainService.getAccessToken(); const [kospiChart, kosdaqChart, kospi200Chart, ksq150Chart] = await Promise.all([ @@ -66,7 +66,7 @@ export class StockIndexController { @Cron('*/5 9-16 * * 1-5') async cronStockIndexLists() { - await this.koreaInvestmentService.getAccessToken(); + await this.koreaInvestmentDomainService.getAccessToken(); const stockLists = await Promise.all([ this.stockIndexService.getDomesticStockIndexListByCode('0001'), // 코스피 diff --git a/BE/src/stock/index/stock-index.module.ts b/BE/src/stock/index/stock-index.module.ts index d763d972..dd3d21f4 100644 --- a/BE/src/stock/index/stock-index.module.ts +++ b/BE/src/stock/index/stock-index.module.ts @@ -1,8 +1,8 @@ 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 { SocketModule } from '../../websocket/socket.module'; +import { KoreaInvestmentModule } from '../../common/koreaInvestment/korea-investment.module'; +import { SocketModule } from '../../common/websocket/socket.module'; import { StockIndexSocketService } from './stock-index-socket.service'; @Module({ diff --git a/BE/src/stock/index/stock-index.service.ts b/BE/src/stock/index/stock-index.service.ts index d3146717..428f6e6e 100644 --- a/BE/src/stock/index/stock-index.service.ts +++ b/BE/src/stock/index/stock-index.service.ts @@ -5,14 +5,14 @@ import { StockIndexChartInterface, StockIndexValueInterface, } from './interface/stock-index.interface'; -import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; +import { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea-investment.domain-service'; @Injectable() export class StockIndexService { private readonly logger = new Logger(); constructor( - private readonly koreaInvestmentService: KoreaInvestmentService, + private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, ) {} async getDomesticStockIndexListByCode(code: string) { @@ -24,7 +24,7 @@ export class StockIndexService { }; const result = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHPUP02110200', '/uapi/domestic-stock/v1/quotations/inquire-index-timeprice', queryParams, @@ -57,7 +57,7 @@ export class StockIndexService { }; const result = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHPUP02100000', '/uapi/domestic-stock/v1/quotations/inquire-index-price', queryParams, diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts index dada0cfd..60c6afa1 100644 --- a/BE/src/stock/order/stock-order-socket.service.ts +++ b/BE/src/stock/order/stock-order-socket.service.ts @@ -4,8 +4,8 @@ import { Logger, } from '@nestjs/common'; import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; -import { BaseSocketService } from '../../websocket/base-socket.service'; -import { SocketGateway } from '../../websocket/socket.gateway'; +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'; @@ -19,18 +19,18 @@ export class StockOrderSocketService { constructor( private readonly socketGateway: SocketGateway, - private readonly baseSocketService: BaseSocketService, + private readonly baseSocketDomainService: BaseSocketDomainService, private readonly stockOrderRepository: StockOrderRepository, ) { - baseSocketService.registerSocketOpenHandler(async () => { + baseSocketDomainService.registerSocketOpenHandler(async () => { const orders: Order[] = await this.stockOrderRepository.findAllCodeByStatus(); orders.forEach((order) => { - baseSocketService.registerCode(this.TR_ID, order.stock_code); + baseSocketDomainService.registerCode(this.TR_ID, order.stock_code); }); }); - baseSocketService.registerSocketDataHandler( + baseSocketDomainService.registerSocketDataHandler( this.TR_ID, (data: string[]) => { this.checkExecutableOrder( @@ -44,11 +44,11 @@ export class StockOrderSocketService { } subscribeByCode(trKey: string) { - this.baseSocketService.registerCode(this.TR_ID, trKey); + this.baseSocketDomainService.registerCode(this.TR_ID, trKey); } unsubscribeByCode(trKey: string) { - this.baseSocketService.unregisterCode(this.TR_ID, trKey); + this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); } private async checkExecutableOrder(stockCode: string, value) { diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index 155c0855..dd4a7142 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -4,7 +4,7 @@ import { StockOrderController } from './stock-order.controller'; import { StockOrderService } from './stock-order.service'; import { Order } from './stock-order.entity'; import { StockOrderRepository } from './stock-order.repository'; -import { SocketModule } from '../../websocket/socket.module'; +import { SocketModule } from '../../common/websocket/socket.module'; import { AssetModule } from '../../asset/asset.module'; import { StockOrderSocketService } from './stock-order-socket.service'; import { UserStockModule } from '../../userStock/user-stock.module'; diff --git a/BE/src/stock/topfive/stock-topfive.module.ts b/BE/src/stock/topfive/stock-topfive.module.ts index 5be62a4f..1b2fb051 100644 --- a/BE/src/stock/topfive/stock-topfive.module.ts +++ b/BE/src/stock/topfive/stock-topfive.module.ts @@ -2,7 +2,7 @@ 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 { KoreaInvestmentModule } from '../../common/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 index 16489241..077ad93f 100644 --- a/BE/src/stock/topfive/stock-topfive.service.ts +++ b/BE/src/stock/topfive/stock-topfive.service.ts @@ -7,14 +7,14 @@ import { StockApiOutputData, StockApiResponse, } from './interface/stock-topfive.interface'; -import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; +import { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea-investment.domain-service'; @Injectable() export class StockTopfiveService { private readonly logger = new Logger(); constructor( - private readonly koreaInvestmentService: KoreaInvestmentService, + private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, ) {} /** @@ -47,7 +47,7 @@ export class StockTopfiveService { } const highResponse = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHPST01700000', '/uapi/domestic-stock/v1/ranking/fluctuation', this.getStockRankingParams({ @@ -57,7 +57,7 @@ export class StockTopfiveService { ); const lowResponse = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHPST01700000', '/uapi/domestic-stock/v1/ranking/fluctuation', this.getStockRankingParams({ 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 965889a1..12c6c886 100644 --- a/BE/src/stock/trade/history/stock-trade-history.module.ts +++ b/BE/src/stock/trade/history/stock-trade-history.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { KoreaInvestmentModule } from '../../../koreaInvestment/korea-investment.module'; +import { KoreaInvestmentModule } from '../../../common/koreaInvestment/korea-investment.module'; import { StockTradeHistoryController } from './stock-trade-history.controller'; import { StockTradeHistoryService } from './stock-trade-history.service'; 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 2b53bd3d..97548ff4 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { KoreaInvestmentService } from '../../../koreaInvestment/korea-investment.service'; +import { KoreaInvestmentDomainService } from '../../../common/koreaInvestment/korea-investment.domain-service'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; import { TodayStockTradeHistoryOutputDto } from './dto/today-stock-trade-history-output.dto'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; @@ -12,7 +12,7 @@ export class StockTradeHistoryService { private readonly logger = new Logger(); constructor( - private readonly koreaInvestmentService: KoreaInvestmentService, + private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, ) {} /** @@ -30,7 +30,7 @@ export class StockTradeHistoryService { }; const response = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHKST01010300', '/uapi/domestic-stock/v1/quotations/inquire-ccnl', queryParams, @@ -88,7 +88,7 @@ export class StockTradeHistoryService { }; const response = - await this.koreaInvestmentService.requestApi( + await this.koreaInvestmentDomainService.requestApi( 'FHKST01010400', '/uapi/domestic-stock/v1/quotations/inquire-daily-price', queryParams, diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts deleted file mode 100644 index e7a2dcc7..00000000 --- a/BE/src/websocket/socket.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SocketGateway } from './socket.gateway'; -import { SocketTokenService } from './socket-token.service'; -import { BaseSocketService } from './base-socket.service'; - -@Module({ - providers: [SocketTokenService, SocketGateway, BaseSocketService], - exports: [SocketGateway, BaseSocketService], -}) -export class SocketModule {} From 52152b927d13abf880bd7a64fa87cafba49ba83d Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 18 Nov 2024 18:39:26 +0900 Subject: [PATCH 24/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20stock=20?= =?UTF-8?q?index=20controller=20=EB=A1=9C=EC=A7=81=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B3=A0=20service=EB=A1=9C=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/index/stock-index.controller.ts | 63 ++----------------- BE/src/stock/index/stock-index.service.ts | 64 +++++++++++++++++++- 2 files changed, 66 insertions(+), 61 deletions(-) diff --git a/BE/src/stock/index/stock-index.controller.ts b/BE/src/stock/index/stock-index.controller.ts index 09d5bd40..0de30ccc 100644 --- a/BE/src/stock/index/stock-index.controller.ts +++ b/BE/src/stock/index/stock-index.controller.ts @@ -3,17 +3,11 @@ 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 { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea-investment.domain-service'; -import { SocketGateway } from '../../common/websocket/socket.gateway'; @Controller('/api/stocks/index') @ApiTags('주가 지수 API') export class StockIndexController { - constructor( - private readonly stockIndexService: StockIndexService, - private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, - private readonly socketGateway: SocketGateway, - ) {} + constructor(private readonly stockIndexService: StockIndexService) {} @Get() @ApiOperation({ @@ -25,61 +19,12 @@ export class StockIndexController { description: '주가 지수 조회 성공', type: StockIndexResponseDto, }) - async getStockIndex() { - await this.koreaInvestmentDomainService.getAccessToken(); - - const [kospiChart, kosdaqChart, kospi200Chart, ksq150Chart] = - await Promise.all([ - this.stockIndexService.getDomesticStockIndexListByCode('0001'), // 코스피 - this.stockIndexService.getDomesticStockIndexListByCode('1001'), // 코스닥 - this.stockIndexService.getDomesticStockIndexListByCode('2001'), // 코스피200 - this.stockIndexService.getDomesticStockIndexListByCode('3003'), // KSQ150 - ]); - - const [kospiValue, kosdaqValue, kospi200Value, ksq150Value] = - await Promise.all([ - this.stockIndexService.getDomesticStockIndexValueByCode('0001'), // 코스피 - this.stockIndexService.getDomesticStockIndexValueByCode('1001'), // 코스닥 - this.stockIndexService.getDomesticStockIndexValueByCode('2001'), // 코스피200 - this.stockIndexService.getDomesticStockIndexValueByCode('3003'), // 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; + getStockIndex() { + return this.stockIndexService.getDomesticStockIndexList(); } @Cron('*/5 9-16 * * 1-5') async cronStockIndexLists() { - await this.koreaInvestmentDomainService.getAccessToken(); - - const stockLists = await Promise.all([ - this.stockIndexService.getDomesticStockIndexListByCode('0001'), // 코스피 - this.stockIndexService.getDomesticStockIndexListByCode('1001'), // 코스닥 - this.stockIndexService.getDomesticStockIndexListByCode('2001'), // 코스피200 - this.stockIndexService.getDomesticStockIndexListByCode('3003'), // KSQ150 - ]); - - this.socketGateway.sendStockIndexListToClient({ - KOSPI: stockLists[0], - KOSDAQ: stockLists[1], - KOSPI200: stockLists[2], - KSQ150: stockLists[3], - }); + await this.stockIndexService.cronDomesticStockIndexList(); } } diff --git a/BE/src/stock/index/stock-index.service.ts b/BE/src/stock/index/stock-index.service.ts index 428f6e6e..cdafe9e8 100644 --- a/BE/src/stock/index/stock-index.service.ts +++ b/BE/src/stock/index/stock-index.service.ts @@ -6,6 +6,8 @@ import { StockIndexValueInterface, } from './interface/stock-index.interface'; import { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea-investment.domain-service'; +import { StockIndexResponseDto } from './dto/stock-index-response.dto'; +import { SocketGateway } from '../../common/websocket/socket.gateway'; @Injectable() export class StockIndexService { @@ -13,9 +15,67 @@ export class StockIndexService { constructor( private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, + private readonly socketGateway: SocketGateway, ) {} - async getDomesticStockIndexListByCode(code: string) { + async getDomesticStockIndexList() { + await this.koreaInvestmentDomainService.getAccessToken(); + + const [kospiChart, kosdaqChart, kospi200Chart, ksq150Chart] = + await Promise.all([ + this.getDomesticStockIndexListByCode('0001'), // 코스피 + this.getDomesticStockIndexListByCode('1001'), // 코스닥 + this.getDomesticStockIndexListByCode('2001'), // 코스피200 + this.getDomesticStockIndexListByCode('3003'), // KSQ150 + ]); + + const [kospiValue, kosdaqValue, kospi200Value, ksq150Value] = + await Promise.all([ + this.getDomesticStockIndexValueByCode('0001'), // 코스피 + this.getDomesticStockIndexValueByCode('1001'), // 코스닥 + this.getDomesticStockIndexValueByCode('2001'), // 코스피200 + this.getDomesticStockIndexValueByCode('3003'), // 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; + } + + async cronDomesticStockIndexList() { + await this.koreaInvestmentDomainService.getAccessToken(); + + const stockLists = await Promise.all([ + this.getDomesticStockIndexListByCode('0001'), // 코스피 + this.getDomesticStockIndexListByCode('1001'), // 코스닥 + this.getDomesticStockIndexListByCode('2001'), // 코스피200 + this.getDomesticStockIndexListByCode('3003'), // KSQ150 + ]); + + this.socketGateway.sendStockIndexListToClient({ + KOSPI: stockLists[0], + KOSDAQ: stockLists[1], + KOSPI200: stockLists[2], + KSQ150: stockLists[3], + }); + } + + private async getDomesticStockIndexListByCode(code: string) { try { const queryParams = { fid_input_hour_1: '300', @@ -49,7 +109,7 @@ export class StockIndexService { } } - async getDomesticStockIndexValueByCode(code: string) { + private async getDomesticStockIndexValueByCode(code: string) { try { const queryParams = { fid_cond_mrkt_div_code: 'U', From 3e85e3db57b4c8eb169a2f5382549c48acc1e763 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 18 Nov 2024 20:29:07 +0900 Subject: [PATCH 25/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D=20?= =?UTF-8?q?=EC=A2=85=EB=AA=A9=EC=BD=94=EB=93=9C=EB=8F=84=20=ED=95=A8?= =?UTF-8?q?=EA=BB=98=20=EB=B0=98=ED=99=98=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/topfive/dto/stock-ranking-data.dto.ts | 3 +++ BE/src/stock/topfive/stock-topfive.service.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/BE/src/stock/topfive/dto/stock-ranking-data.dto.ts b/BE/src/stock/topfive/dto/stock-ranking-data.dto.ts index 5f1a3026..268283e7 100644 --- a/BE/src/stock/topfive/dto/stock-ranking-data.dto.ts +++ b/BE/src/stock/topfive/dto/stock-ranking-data.dto.ts @@ -4,6 +4,9 @@ import { ApiProperty } from '@nestjs/swagger'; * 등락률 API 요청 후 받은 응답값 정제용 DTO */ export class StockRankingDataDto { + @ApiProperty({ description: '주식 종목 코드' }) + stck_shrn_iscd: string; + @ApiProperty({ description: 'HTS 한글 종목명' }) hts_kor_isnm: string; diff --git a/BE/src/stock/topfive/stock-topfive.service.ts b/BE/src/stock/topfive/stock-topfive.service.ts index 82f659d5..5058752a 100644 --- a/BE/src/stock/topfive/stock-topfive.service.ts +++ b/BE/src/stock/topfive/stock-topfive.service.ts @@ -118,6 +118,7 @@ export class StockTopfiveService { private formatStockData(stocks: StockApiOutputData[]) { return stocks.slice(0, 5).map((stock) => { const stockData = new StockRankingDataDto(); + stockData.stck_shrn_iscd = stock.stck_shrn_iscd; stockData.hts_kor_isnm = stock.hts_kor_isnm; stockData.stck_prpr = stock.stck_prpr; stockData.prdy_vrss = stock.prdy_vrss; From dab1f7b0c5339a21e53ece8a987b4b495df07e57 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 11:23:38 +0900 Subject: [PATCH 26/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20websocket=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/trade/history/stock-trade-history-socket.service.ts | 2 +- BE/src/stock/trade/history/stock-trade-history.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 4489d613..dab0aea2 100644 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts @@ -3,7 +3,7 @@ import axios from 'axios'; import { Observable, Subject } from 'rxjs'; import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { SseEvent } from './interface/sse-event'; -import { SocketConnectTokenInterface } from '../../../websocket/interface/socket.interface'; +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'; 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 3d462105..ca5ce277 100644 --- a/BE/src/stock/trade/history/stock-trade-history.module.ts +++ b/BE/src/stock/trade/history/stock-trade-history.module.ts @@ -3,7 +3,7 @@ import { KoreaInvestmentModule } from '../../../common/koreaInvestment/korea-inv 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 '../../../websocket/socket.module'; +import { SocketModule } from '../../../common/websocket/socket.module'; @Module({ imports: [KoreaInvestmentModule, SocketModule], From f7b96730a0dcbd652087c767aeec480e0c8c05a5 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 11:35:03 +0900 Subject: [PATCH 27/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20try-catch=EB=AC=B8=20=EB=B0=8F=20logger=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../korea-investment.domain-service.ts | 31 ++---- BE/src/stock/detail/stock-detail.service.ts | 75 +++++---------- BE/src/stock/index/stock-index.service.ts | 96 +++++++------------ BE/src/stock/topfive/stock-topfive.service.ts | 91 ++++++++---------- .../history/stock-trade-history.service.ts | 74 +++++--------- 5 files changed, 136 insertions(+), 231 deletions(-) diff --git a/BE/src/common/koreaInvestment/korea-investment.domain-service.ts b/BE/src/common/koreaInvestment/korea-investment.domain-service.ts index 3e96c55b..26e890d3 100644 --- a/BE/src/common/koreaInvestment/korea-investment.domain-service.ts +++ b/BE/src/common/koreaInvestment/korea-investment.domain-service.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Logger, UnauthorizedException } from '@nestjs/common'; +import { UnauthorizedException } from '@nestjs/common'; import { getFullURL } from '../../util/get-full-URL'; import { AccessTokenInterface } from './interface/korea-investment.interface'; import { getHeader } from '../../util/get-header'; @@ -8,8 +8,6 @@ export class KoreaInvestmentDomainService { private accessToken: string; private tokenExpireTime: Date; - private readonly logger = new Logger(); - async getAccessToken() { // accessToken이 유효한 경우 if (this.accessToken && this.tokenExpireTime > new Date()) { @@ -47,26 +45,15 @@ export class KoreaInvestmentDomainService { apiURL: string, params: Record, ): Promise { - try { - const accessToken = await this.getAccessToken(); - const headers = getHeader(accessToken, trId); - const url = getFullURL(apiURL); + const accessToken = await this.getAccessToken(); + const headers = getHeader(accessToken, trId); + const url = getFullURL(apiURL); - const response = await axios.get(url, { - headers, - params, - }); + 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; - } + return response.data; } } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 389d7147..2735113e 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -26,30 +26,18 @@ export class StockDetailService { * @author uuuo3o */ async getInquirePrice(stockCode: string) { - try { - const queryParams = { - fid_cond_mrkt_div_code: 'J', - fid_input_iscd: stockCode, - }; - - const response = - await this.koreaInvestmentDomainService.requestApi( - 'FHKST01010100', - '/uapi/domestic-stock/v1/quotations/inquire-price', - queryParams, - ); + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + }; - return await this.formatStockData(response.output); - } 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; - } + const response = + await this.koreaInvestmentDomainService.requestApi( + 'FHKST01010100', + '/uapi/domestic-stock/v1/quotations/inquire-price', + queryParams, + ); + return this.formatStockData(response.output); } /** @@ -94,34 +82,23 @@ export class StockDetailService { date2: string, periodDivCode: string, ) { - try { - const queryParams = { - fid_cond_mrkt_div_code: 'J', - fid_input_iscd: stockCode, - fid_input_date_1: date1, - fid_input_date_2: date2, - fid_period_div_code: periodDivCode, - fid_org_adj_prc: '0', - }; + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + fid_input_date_1: date1, + fid_input_date_2: date2, + fid_period_div_code: periodDivCode, + fid_org_adj_prc: '0', + }; - const response = - await this.koreaInvestmentDomainService.requestApi( - 'FHKST03010100', - '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', - queryParams, - ); + const response = + await this.koreaInvestmentDomainService.requestApi( + 'FHKST03010100', + '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', + queryParams, + ); - return this.formatStockInquirePriceData(response).slice().reverse(); - } 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; - } + return this.formatStockInquirePriceData(response).slice().reverse(); } /** diff --git a/BE/src/stock/index/stock-index.service.ts b/BE/src/stock/index/stock-index.service.ts index cdafe9e8..f68aa9d1 100644 --- a/BE/src/stock/index/stock-index.service.ts +++ b/BE/src/stock/index/stock-index.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { StockIndexListChartElementDto } from './dto/stock-index-list-chart.element.dto'; import { StockIndexValueElementDto } from './dto/stock-index-value-element.dto'; import { @@ -11,8 +11,6 @@ import { SocketGateway } from '../../common/websocket/socket.gateway'; @Injectable() export class StockIndexService { - private readonly logger = new Logger(); - constructor( private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, private readonly socketGateway: SocketGateway, @@ -76,70 +74,48 @@ export class StockIndexService { } private async getDomesticStockIndexListByCode(code: string) { - try { - const queryParams = { - fid_input_hour_1: '300', - fid_cond_mrkt_div_code: 'U', - fid_input_iscd: code, - }; + const queryParams = { + fid_input_hour_1: '300', + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }; - const result = - await this.koreaInvestmentDomainService.requestApi( - 'FHPUP02110200', - '/uapi/domestic-stock/v1/quotations/inquire-index-timeprice', - queryParams, - ); + const result = + await this.koreaInvestmentDomainService.requestApi( + 'FHPUP02110200', + '/uapi/domestic-stock/v1/quotations/inquire-index-timeprice', + queryParams, + ); - return result.output.map((element) => { - return new StockIndexListChartElementDto( - element.bsop_hour, - element.bstp_nmix_prpr, - element.bstp_nmix_prdy_vrss, - ); - }); - } 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; - } + return result.output.map((element) => { + return new StockIndexListChartElementDto( + element.bsop_hour, + element.bstp_nmix_prpr, + element.bstp_nmix_prdy_vrss, + ); + }); } private async getDomesticStockIndexValueByCode(code: string) { - try { - const queryParams = { - fid_cond_mrkt_div_code: 'U', - fid_input_iscd: code, - }; + const queryParams = { + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }; - const result = - await this.koreaInvestmentDomainService.requestApi( - 'FHPUP02100000', - '/uapi/domestic-stock/v1/quotations/inquire-index-price', - queryParams, - ); + const result = + await this.koreaInvestmentDomainService.requestApi( + 'FHPUP02100000', + '/uapi/domestic-stock/v1/quotations/inquire-index-price', + queryParams, + ); - const data = result.output; + 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, - ); - } 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; - } + return new StockIndexValueElementDto( + data.bstp_nmix_prpr, + data.bstp_nmix_prdy_vrss, + data.bstp_nmix_prdy_ctrt, + data.prdy_vrss_sign, + ); } } diff --git a/BE/src/stock/topfive/stock-topfive.service.ts b/BE/src/stock/topfive/stock-topfive.service.ts index 28b8e1ff..2feeef86 100644 --- a/BE/src/stock/topfive/stock-topfive.service.ts +++ b/BE/src/stock/topfive/stock-topfive.service.ts @@ -25,62 +25,51 @@ export class StockTopfiveService { * @author uuuo3o */ async getMarketRanking(marketType: MarketType) { - try { - const queryParams = new StockRankingQueryParameterDto(); - queryParams.fid_cond_mrkt_div_code = 'J'; + 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; - } + 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.koreaInvestmentDomainService.requestApi( - 'FHPST01700000', - '/uapi/domestic-stock/v1/ranking/fluctuation', - this.getStockRankingParams({ - ...queryParams, - fid_rank_sort_cls_code: '0', - }), - ); + const highResponse = + await this.koreaInvestmentDomainService.requestApi( + 'FHPST01700000', + '/uapi/domestic-stock/v1/ranking/fluctuation', + this.getStockRankingParams({ + ...queryParams, + fid_rank_sort_cls_code: '0', + }), + ); - const lowResponse = - await this.koreaInvestmentDomainService.requestApi( - 'FHPST01700000', - '/uapi/domestic-stock/v1/ranking/fluctuation', - this.getStockRankingParams({ - ...queryParams, - fid_rank_sort_cls_code: '1', - }), - ); + const lowResponse = + await this.koreaInvestmentDomainService.requestApi( + 'FHPST01700000', + '/uapi/domestic-stock/v1/ranking/fluctuation', + this.getStockRankingParams({ + ...queryParams, + fid_rank_sort_cls_code: '1', + }), + ); - const response = new StockRankingResponseDto(); - response.high = this.formatStockData(highResponse.output); - response.low = this.formatStockData(lowResponse.output); + 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; - } + return response; } /** 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 97548ff4..564ba4ca 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { KoreaInvestmentDomainService } from '../../../common/koreaInvestment/korea-investment.domain-service'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; import { TodayStockTradeHistoryOutputDto } from './dto/today-stock-trade-history-output.dto'; @@ -9,8 +9,6 @@ import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-d @Injectable() export class StockTradeHistoryService { - private readonly logger = new Logger(); - constructor( private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, ) {} @@ -23,30 +21,19 @@ export class StockTradeHistoryService { * @author uuuo3o */ async getTodayStockTradeHistory(stockCode: string) { - try { - const queryParams = { - fid_cond_mrkt_div_code: 'J', - fid_input_iscd: stockCode, - }; + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + }; - const response = - await this.koreaInvestmentDomainService.requestApi( - 'FHKST01010300', - '/uapi/domestic-stock/v1/quotations/inquire-ccnl', - queryParams, - ); + const response = + await this.koreaInvestmentDomainService.requestApi( + 'FHKST01010300', + '/uapi/domestic-stock/v1/quotations/inquire-ccnl', + queryParams, + ); - return this.formatTodayStockTradeHistoryData(response.output); - } 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; - } + return this.formatTodayStockTradeHistoryData(response.output); } /** @@ -79,32 +66,21 @@ export class StockTradeHistoryService { * @author uuuo3o */ async getDailyStockTradeHistory(stockCode: string) { - try { - const queryParams = { - fid_cond_mrkt_div_code: 'J', - fid_input_iscd: stockCode, - fid_period_div_code: 'D', - fid_org_adj_prc: '0', - }; + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + fid_period_div_code: 'D', + fid_org_adj_prc: '0', + }; - const response = - await this.koreaInvestmentDomainService.requestApi( - 'FHKST01010400', - '/uapi/domestic-stock/v1/quotations/inquire-daily-price', - queryParams, - ); + const response = + await this.koreaInvestmentDomainService.requestApi( + 'FHKST01010400', + '/uapi/domestic-stock/v1/quotations/inquire-daily-price', + queryParams, + ); - return this.formatDailyStockTradeHistoryData(response.output); - } 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; - } + return this.formatDailyStockTradeHistoryData(response.output); } /** From c07c5bc5b23ea0cbd8b35e42aa5393a54d0d6699 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 11:37:34 +0900 Subject: [PATCH 28/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20logger=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 4 +--- BE/src/stock/topfive/stock-topfive.service.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 2735113e..4de8f87f 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea-investment.domain-service'; import { InquirePriceChartApiResponse } from './interface/stock-detail-chart.interface'; import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; @@ -11,8 +11,6 @@ import { StockDetailRepository } from './stock-detail.repository'; @Injectable() export class StockDetailService { - private readonly logger = new Logger(); - constructor( private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, private readonly stockDetailRepository: StockDetailRepository, diff --git a/BE/src/stock/topfive/stock-topfive.service.ts b/BE/src/stock/topfive/stock-topfive.service.ts index 2feeef86..15052081 100644 --- a/BE/src/stock/topfive/stock-topfive.service.ts +++ b/BE/src/stock/topfive/stock-topfive.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } 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'; @@ -11,8 +11,6 @@ import { KoreaInvestmentDomainService } from '../../common/koreaInvestment/korea @Injectable() export class StockTopfiveService { - private readonly logger = new Logger(); - constructor( private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, ) {} From 8d60390153e3cd3cc7b838c8a8a5a4b29d33bb31 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 13:04:21 +0900 Subject: [PATCH 29/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4=EB=8F=84/?= =?UTF-8?q?=EB=A7=A4=EC=88=98=20=EC=8B=9C=20=EC=A2=85=EB=AA=A9=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20isNotEmpty=20=ED=99=95=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/dto/stock-order-request.dto.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/order/dto/stock-order-request.dto.ts b/BE/src/stock/order/dto/stock-order-request.dto.ts index 407d05ad..458f54af 100644 --- a/BE/src/stock/order/dto/stock-order-request.dto.ts +++ b/BE/src/stock/order/dto/stock-order-request.dto.ts @@ -1,8 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsNumber, IsPositive } from 'class-validator'; +import { IsInt, IsNotEmpty, IsNumber, IsPositive } from 'class-validator'; export class StockOrderRequestDto { @ApiProperty({ description: '주식 id', example: '005930' }) + @IsNotEmpty() stock_code: string; @ApiProperty({ description: '매수/매도 희망 가격' }) From 4243005e79122a8ea93c108ca0b8795195addd41 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 13:24:31 +0900 Subject: [PATCH 30/81] =?UTF-8?q?=E2=9E=95=20add=20:=20=EB=9E=AD=ED=82=B9?= =?UTF-8?q?=20API=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=AA=A8=EB=93=88,=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC,=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 ++ BE/src/ranking/ranking.controller.ts | 4 ++++ BE/src/ranking/ranking.module.ts | 14 ++++++++++++++ BE/src/ranking/ranking.service.ts | 4 ++++ 4 files changed, 24 insertions(+) create mode 100644 BE/src/ranking/ranking.controller.ts create mode 100644 BE/src/ranking/ranking.module.ts create mode 100644 BE/src/ranking/ranking.service.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 39527728..40ed1831 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -17,6 +17,7 @@ import { StockListModule } from './stock/list/stock-list.module'; import { StockTradeHistoryModule } from './stock/trade/history/stock-trade-history.module'; import { RedisModule } from './common/redis/redis.module'; import { HTTPExceptionFilter } from './common/filters/http-exception.filter'; +import { RankingModule } from './ranking/ranking.module'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { HTTPExceptionFilter } from './common/filters/http-exception.filter'; StockListModule, StockTradeHistoryModule, RedisModule, + RankingModule, ], controllers: [AppController], providers: [ diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts new file mode 100644 index 00000000..5775064f --- /dev/null +++ b/BE/src/ranking/ranking.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('ranking') +export class RankingController {} diff --git a/BE/src/ranking/ranking.module.ts b/BE/src/ranking/ranking.module.ts new file mode 100644 index 00000000..d89566b8 --- /dev/null +++ b/BE/src/ranking/ranking.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { RankingController } from './ranking.controller'; +import { RankingService } from './ranking.service'; +import { Asset } from 'src/asset/asset.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RedisModule } from 'src/common/redis/redis.module'; +import { RedisDomainService } from 'src/common/redis/redis.domain-service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Asset]), RedisModule], + controllers: [RankingController], + providers: [RankingService, RedisDomainService], +}) +export class RankingModule {} diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts new file mode 100644 index 00000000..6c92552c --- /dev/null +++ b/BE/src/ranking/ranking.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class RankingService {} From 49edc8fe8460e725006501b6ae813e86fcd68ecf Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 19 Nov 2024 13:36:44 +0900 Subject: [PATCH 31/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20?= =?UTF-8?q?=EC=83=81=ED=95=9C=EA=B0=80,=20=ED=95=98=ED=95=9C=EA=B0=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80#144?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/dto/stock-detail-response.dto.ts | 6 ++++++ BE/src/stock/detail/stock-detail.service.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/BE/src/stock/detail/dto/stock-detail-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts index 96e4ba52..6c7d1aa6 100644 --- a/BE/src/stock/detail/dto/stock-detail-response.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-response.dto.ts @@ -24,4 +24,10 @@ export class InquirePriceResponseDto { @ApiProperty({ description: 'PER' }) per: string; + + @ApiProperty({ description: '주식 상한가' }) + stck_mxpr: string; + + @ApiProperty({ description: '주식 하한가' }) + stck_llam: string; } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 4de8f87f..b3e537ed 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -61,6 +61,8 @@ export class StockDetailService { prdy_ctrt: stock.prdy_ctrt, hts_avls: stock.hts_avls, per: stock.per, + stck_mxpr: stock.stck_mxpr, + stck_llam: stock.stck_llam, }; } From b40079d1d3a5c6e46df7add2a6793cb89354e200 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 14:02:32 +0900 Subject: [PATCH 32/81] =?UTF-8?q?=E2=9C=A8=20feat=20:=20rankings=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=84=20=EC=9C=84=ED=95=9C=20Ent?= =?UTF-8?q?ity=20=EC=9E=91=EC=84=B1(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.entity.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 BE/src/ranking/ranking.entity.ts diff --git a/BE/src/ranking/ranking.entity.ts b/BE/src/ranking/ranking.entity.ts new file mode 100644 index 00000000..ed771d74 --- /dev/null +++ b/BE/src/ranking/ranking.entity.ts @@ -0,0 +1,26 @@ +import { User } from 'src/auth/user.entity'; +import { + BaseEntity, + Column, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity() +export class Rankings extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => User, (user) => user.id) + user: User; + + @Column() + profit: number; + + @Column() + profitRate: number; + + @Column() + period: number; +} From cc6665bd7b9936fbc5187ef2fc5df0a937c59ead Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 14:04:40 +0900 Subject: [PATCH 33/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A7=A4=EB=8F=84=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=A3=BC=EC=8B=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EB=B0=98=ED=99=98=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/userStock/user-stock.controller.ts | 37 +++++++++++++++++++++-- BE/src/userStock/user-stock.service.ts | 14 ++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/BE/src/userStock/user-stock.controller.ts b/BE/src/userStock/user-stock.controller.ts index e1e4c209..b771aecd 100644 --- a/BE/src/userStock/user-stock.controller.ts +++ b/BE/src/userStock/user-stock.controller.ts @@ -1,6 +1,37 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { Request } from 'express'; +import { JwtAuthGuard } from '../auth/jwt-auth-guard'; +import { UserStockService } from './user-stock.service'; @Controller('/api/userStock') @ApiTags('사용자 보유 주식 API') -export class UserStockController {} +export class UserStockController { + constructor(private readonly userStockService: UserStockService) {} + + @Get('/:stockCode') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '매도 가능 주식 개수 조회 API', + description: '특정 주식 매도 시에 필요한 매도 가능한 주식 개수를 조회한다.', + }) + @ApiResponse({ + status: 200, + description: '매도 가능 주식 개수 조회 성공', + }) + async getUserStockByCode( + @Req() request: Request, + @Param('stockCode') stockCode: string, + ) { + return this.userStockService.getUserStockByCode( + parseInt(request.user.userId, 10), + stockCode, + ); + } +} diff --git a/BE/src/userStock/user-stock.service.ts b/BE/src/userStock/user-stock.service.ts index 1c69dcaf..7d69fa62 100644 --- a/BE/src/userStock/user-stock.service.ts +++ b/BE/src/userStock/user-stock.service.ts @@ -1,4 +1,16 @@ import { Injectable } from '@nestjs/common'; +import { UserStockRepository } from './user-stock.repository'; @Injectable() -export class UserStockService {} +export class UserStockService { + constructor(private readonly userStockRepository: UserStockRepository) {} + + async getUserStockByCode(userId: number, stockCode: string) { + const userStock = await this.userStockRepository.findOneBy({ + user_id: userId, + stock_code: stockCode, + }); + + return { quantity: userStock ? userStock.quantity : 0 }; + } +} From b168dfe0b33f89dd1434b0faf45a0d11a2a81d74 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 14:14:45 +0900 Subject: [PATCH 34/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20user-sto?= =?UTF-8?q?ck=EC=9D=84=20asset=EA=B3=BC=20=ED=95=A8=EA=BB=98=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 39 +++++++++++++++++-- BE/src/asset/asset.module.ts | 8 ++-- BE/src/asset/asset.service.ts | 18 ++++++++- .../{userStock => asset}/user-stock.entity.ts | 0 .../user-stock.repository.ts | 0 BE/src/stock/order/stock-order.module.ts | 8 +--- BE/src/stock/order/stock-order.repository.ts | 2 +- BE/src/stock/order/stock-order.service.ts | 2 +- BE/src/userStock/user-stock.controller.ts | 37 ------------------ BE/src/userStock/user-stock.module.ts | 14 ------- BE/src/userStock/user-stock.service.ts | 16 -------- 11 files changed, 60 insertions(+), 84 deletions(-) rename BE/src/{userStock => asset}/user-stock.entity.ts (100%) rename BE/src/{userStock => asset}/user-stock.repository.ts (100%) delete mode 100644 BE/src/userStock/user-stock.controller.ts delete mode 100644 BE/src/userStock/user-stock.module.ts delete mode 100644 BE/src/userStock/user-stock.service.ts diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index 3bca562e..b3f7f0b8 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -1,6 +1,37 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { Request } from 'express'; +import { JwtAuthGuard } from '../auth/jwt-auth-guard'; +import { AssetService } from './asset.service'; @Controller('/api/assets') -@ApiTags('자산 API') -export class AssetController {} +@ApiTags('사용자 자산 및 보유 주식 API') +export class AssetController { + constructor(private readonly assetService: AssetService) {} + + @Get('/stocks/:stockCode') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '매도 가능 주식 개수 조회 API', + description: '특정 주식 매도 시에 필요한 매도 가능한 주식 개수를 조회한다.', + }) + @ApiResponse({ + status: 200, + description: '매도 가능 주식 개수 조회 성공', + }) + async getUserStockByCode( + @Req() request: Request, + @Param('stockCode') stockCode: string, + ) { + return this.assetService.getUserStockByCode( + parseInt(request.user.userId, 10), + stockCode, + ); + } +} diff --git a/BE/src/asset/asset.module.ts b/BE/src/asset/asset.module.ts index 5ddf8874..79f305fc 100644 --- a/BE/src/asset/asset.module.ts +++ b/BE/src/asset/asset.module.ts @@ -4,11 +4,13 @@ import { AssetController } from './asset.controller'; import { AssetService } from './asset.service'; import { AssetRepository } from './asset.repository'; import { Asset } from './asset.entity'; +import { UserStock } from './user-stock.entity'; +import { UserStockRepository } from './user-stock.repository'; @Module({ - imports: [TypeOrmModule.forFeature([Asset])], + imports: [TypeOrmModule.forFeature([Asset, UserStock])], controllers: [AssetController], - providers: [AssetService, AssetRepository], - exports: [AssetRepository], + providers: [AssetService, AssetRepository, UserStockRepository], + exports: [AssetRepository, UserStockRepository], }) export class AssetModule {} diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index f424a0d8..3e8c1f35 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -1,4 +1,20 @@ import { Injectable } from '@nestjs/common'; +import { UserStockRepository } from './user-stock.repository'; +import { AssetRepository } from './asset.repository'; @Injectable() -export class AssetService {} +export class AssetService { + constructor( + private readonly userStockRepository: UserStockRepository, + private readonly assetRepository: AssetRepository, + ) {} + + async getUserStockByCode(userId: number, stockCode: string) { + const userStock = await this.userStockRepository.findOneBy({ + user_id: userId, + stock_code: stockCode, + }); + + return { quantity: userStock ? userStock.quantity : 0 }; + } +} diff --git a/BE/src/userStock/user-stock.entity.ts b/BE/src/asset/user-stock.entity.ts similarity index 100% rename from BE/src/userStock/user-stock.entity.ts rename to BE/src/asset/user-stock.entity.ts diff --git a/BE/src/userStock/user-stock.repository.ts b/BE/src/asset/user-stock.repository.ts similarity index 100% rename from BE/src/userStock/user-stock.repository.ts rename to BE/src/asset/user-stock.repository.ts diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index dd4a7142..375d4a53 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -7,15 +7,9 @@ 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 { UserStockModule } from '../../userStock/user-stock.module'; @Module({ - imports: [ - TypeOrmModule.forFeature([Order]), - SocketModule, - AssetModule, - UserStockModule, - ], + imports: [TypeOrmModule.forFeature([Order]), SocketModule, AssetModule], controllers: [StockOrderController], providers: [StockOrderService, StockOrderRepository, StockOrderSocketService], }) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 94bfd37f..559f06fa 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -4,7 +4,7 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Order } from './stock-order.entity'; import { StatusType } from './enum/status-type'; import { Asset } from '../../asset/asset.entity'; -import { UserStock } from '../../userStock/user-stock.entity'; +import { UserStock } from '../../asset/user-stock.entity'; @Injectable() export class StockOrderRepository extends Repository { diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index dd6df0cc..1f4e8a58 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -10,7 +10,7 @@ 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 '../../userStock/user-stock.repository'; +import { UserStockRepository } from '../../asset/user-stock.repository'; import { AssetRepository } from '../../asset/asset.repository'; @Injectable() diff --git a/BE/src/userStock/user-stock.controller.ts b/BE/src/userStock/user-stock.controller.ts deleted file mode 100644 index b771aecd..00000000 --- a/BE/src/userStock/user-stock.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; -import { - ApiBearerAuth, - ApiOperation, - ApiResponse, - ApiTags, -} from '@nestjs/swagger'; -import { Request } from 'express'; -import { JwtAuthGuard } from '../auth/jwt-auth-guard'; -import { UserStockService } from './user-stock.service'; - -@Controller('/api/userStock') -@ApiTags('사용자 보유 주식 API') -export class UserStockController { - constructor(private readonly userStockService: UserStockService) {} - - @Get('/:stockCode') - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @ApiOperation({ - summary: '매도 가능 주식 개수 조회 API', - description: '특정 주식 매도 시에 필요한 매도 가능한 주식 개수를 조회한다.', - }) - @ApiResponse({ - status: 200, - description: '매도 가능 주식 개수 조회 성공', - }) - async getUserStockByCode( - @Req() request: Request, - @Param('stockCode') stockCode: string, - ) { - return this.userStockService.getUserStockByCode( - parseInt(request.user.userId, 10), - stockCode, - ); - } -} diff --git a/BE/src/userStock/user-stock.module.ts b/BE/src/userStock/user-stock.module.ts deleted file mode 100644 index a6466976..00000000 --- a/BE/src/userStock/user-stock.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserStock } from './user-stock.entity'; -import { UserStockController } from './user-stock.controller'; -import { UserStockRepository } from './user-stock.repository'; -import { UserStockService } from './user-stock.service'; - -@Module({ - imports: [TypeOrmModule.forFeature([UserStock])], - controllers: [UserStockController], - providers: [UserStockRepository, UserStockService], - exports: [UserStockRepository], -}) -export class UserStockModule {} diff --git a/BE/src/userStock/user-stock.service.ts b/BE/src/userStock/user-stock.service.ts deleted file mode 100644 index 7d69fa62..00000000 --- a/BE/src/userStock/user-stock.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { UserStockRepository } from './user-stock.repository'; - -@Injectable() -export class UserStockService { - constructor(private readonly userStockRepository: UserStockRepository) {} - - async getUserStockByCode(userId: number, stockCode: string) { - const userStock = await this.userStockRepository.findOneBy({ - user_id: userId, - stock_code: stockCode, - }); - - return { quantity: userStock ? userStock.quantity : 0 }; - } -} From 597a5c526cd03d2bd20b84f1539ae7c558ac85a0 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 14:22:55 +0900 Subject: [PATCH 35/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A7=A4=EC=88=98=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=20=EA=B8=88=EC=95=A1=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 16 ++++++++++++++++ BE/src/asset/asset.service.ts | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index b3f7f0b8..4c985490 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -34,4 +34,20 @@ export class AssetController { stockCode, ); } + + @Get('/cash') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '매수 가능 금액 조회 API', + description: + '특정 주식 매수 시에 필요한 매수 가능한 금액(현재 가용자산)을 조회한다.', + }) + @ApiResponse({ + status: 200, + description: '매수 가능 금액 조회 성공', + }) + async getCashBalance(@Req() request: Request) { + return this.assetService.getCashBalance(parseInt(request.user.userId, 10)); + } } diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 3e8c1f35..4fce3635 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -17,4 +17,10 @@ export class AssetService { return { quantity: userStock ? userStock.quantity : 0 }; } + + async getCashBalance(userId: number) { + const asset = await this.assetRepository.findOneBy({ user_id: userId }); + + return { cash_balance: asset ? asset.cash_balance : 0 }; + } } From c5ff17589062f3058b711bf17af3e82d00d830dc Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 15:00:02 +0900 Subject: [PATCH 36/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20asset=20=EB=B3=B4?= =?UTF-8?q?=EC=9C=A0=20=EC=9E=90=EC=82=B0=20=EC=A1=B0=ED=9A=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 4fce3635..5292fa32 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -21,6 +21,6 @@ export class AssetService { async getCashBalance(userId: number) { const asset = await this.assetRepository.findOneBy({ user_id: userId }); - return { cash_balance: asset ? asset.cash_balance : 0 }; + return { cash_balance: asset.cash_balance }; } } From 22dd87d5450aa42b5d920e50fa8d8bb9a7f482d3 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 15:21:38 +0900 Subject: [PATCH 37/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20cookie=EB=A5=BC?= =?UTF-8?q?=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20accessToken=20validate(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 2 +- BE/src/auth/strategy/jwt.strategy.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 8fcdf300..02c5198c 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -47,7 +47,7 @@ export class AuthController { @Get('/test') @UseGuards(AuthGuard('jwt')) test(@Req() req: Request) { - return req; + return 'test'; } @ApiOperation({ summary: 'Kakao 로그인 API' }) diff --git a/BE/src/auth/strategy/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts index 7f8f5d2a..78063d1f 100644 --- a/BE/src/auth/strategy/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -5,6 +5,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { UserRepository } from '../user.repository'; import { User } from '../user.entity'; +import { Request } from 'express'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { @@ -14,7 +15,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { ) { super({ secretOrKey: configService.get('JWT_SECRET'), - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: extractJWTFromCookie, }); } @@ -31,3 +32,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }; } } + +function extractJWTFromCookie(req: Request): string | null { + if (req.cookies && 'accessToken' in req.cookies) { + return req.cookies['accessToken']; + } + return null; +} From d4d977b29cfb0d2bf27708e466994b58891eef4c Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 15:29:48 +0900 Subject: [PATCH 38/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20lint?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 7 ------ BE/src/auth/strategy/jwt.strategy.ts | 32 +++++++++++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 02c5198c..22e29cdb 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -43,13 +43,6 @@ export class AuthController { return res.status(200).json({ accessToken }); } - @ApiOperation({ summary: 'Token 인증 테스트 API' }) - @Get('/test') - @UseGuards(AuthGuard('jwt')) - test(@Req() req: Request) { - return 'test'; - } - @ApiOperation({ summary: 'Kakao 로그인 API' }) @Get('/kakao') @UseGuards(AuthGuard('kakao')) diff --git a/BE/src/auth/strategy/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts index 78063d1f..049e2cf2 100644 --- a/BE/src/auth/strategy/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -1,11 +1,25 @@ import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; -import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Strategy } from 'passport-jwt'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; import { UserRepository } from '../user.repository'; import { User } from '../user.entity'; -import { Request } from 'express'; + +interface RequestWithCookies extends Request { + cookies: { + accessToken?: string; + [key: string]: string | undefined; + }; +} + +function extractJWTFromCookie(req: RequestWithCookies): string | null { + if (req.cookies && 'accessToken' in req.cookies) { + return req.cookies.accessToken; + } + return null; +} @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { @@ -19,7 +33,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }); } - async validate(payload) { + async validate(payload: { email: string }): Promise<{ + userId: number; + email: string; + tutorial: boolean; + kakaoId: string | null; + }> { const { email } = payload; const user: User = await this.userRepository.findOne({ where: { email } }); if (!user) throw new UnauthorizedException(); @@ -32,10 +51,3 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }; } } - -function extractJWTFromCookie(req: Request): string | null { - if (req.cookies && 'accessToken' in req.cookies) { - return req.cookies['accessToken']; - } - return null; -} From 328b1639fcc9ba87dc75763ec6a5dc853c8b4a0f Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 15:40:28 +0900 Subject: [PATCH 39/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20docker=20timezon?= =?UTF-8?q?e=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/Dockerfile | 1 + FE/Dockerfile | 2 ++ 2 files changed, 3 insertions(+) diff --git a/BE/Dockerfile b/BE/Dockerfile index 60079503..afd130d2 100644 --- a/BE/Dockerfile +++ b/BE/Dockerfile @@ -12,6 +12,7 @@ WORKDIR /var/app COPY package*.json ./ RUN npm install --only=production COPY --from=builder /app/dist ./dist +RUN sudo ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime EXPOSE 3000 CMD ["node", "dist/main.js"] \ No newline at end of file diff --git a/FE/Dockerfile b/FE/Dockerfile index e695254d..749bc07a 100644 --- a/FE/Dockerfile +++ b/FE/Dockerfile @@ -25,6 +25,8 @@ RUN npm run build FROM nginx:alpine WORKDIR /app COPY --from=builder /app/dist /usr/share/nginx/html +#time zone 설정 +RUN apk add tzdata && ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file From 650fdf27a7a2d546441deaeaa371da20b9ede35c Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 15:46:07 +0900 Subject: [PATCH 40/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=EC=96=B4=EC=97=90=EC=84=9C=20sudo=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/Dockerfile b/BE/Dockerfile index afd130d2..f530f5ad 100644 --- a/BE/Dockerfile +++ b/BE/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /var/app COPY package*.json ./ RUN npm install --only=production COPY --from=builder /app/dist ./dist -RUN sudo ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime +RUN ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime EXPOSE 3000 CMD ["node", "dist/main.js"] \ No newline at end of file From 9a56a182ef50a2bd388e2d3f301fe090688609d7 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 16:24:35 +0900 Subject: [PATCH 41/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#150?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 17 ++++++++++ BE/src/asset/asset.service.ts | 32 +++++++++++++++++++ BE/src/asset/dto/asset-response.dto.ts | 32 +++++++++++++++++++ BE/src/asset/dto/mypage-response.dto.ts | 17 ++++++++++ .../asset/dto/stock-element-response.dto.ts | 22 +++++++++++++ .../asset/interface/user-stock.interface.ts | 11 +++++++ BE/src/asset/user-stock.repository.ts | 12 +++++++ 7 files changed, 143 insertions(+) create mode 100644 BE/src/asset/dto/asset-response.dto.ts create mode 100644 BE/src/asset/dto/mypage-response.dto.ts create mode 100644 BE/src/asset/dto/stock-element-response.dto.ts create mode 100644 BE/src/asset/interface/user-stock.interface.ts diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index 4c985490..fa07535c 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -8,6 +8,7 @@ import { import { Request } from 'express'; import { JwtAuthGuard } from '../auth/jwt-auth-guard'; import { AssetService } from './asset.service'; +import { MypageResponseDto } from './dto/mypage-response.dto'; @Controller('/api/assets') @ApiTags('사용자 자산 및 보유 주식 API') @@ -50,4 +51,20 @@ export class AssetController { async getCashBalance(@Req() request: Request) { return this.assetService.getCashBalance(parseInt(request.user.userId, 10)); } + + @Get() + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '마이페이지 보유 자산 현황 조회 API', + description: '마이페이지 조회 시 필요한 보유 자산 현황을 조회한다.', + }) + @ApiResponse({ + status: 200, + description: '매수 가능 금액 조회 성공', + type: MypageResponseDto, + }) + async getMyPage(@Req() request: Request) { + return this.assetService.getMyPage(parseInt(request.user.userId, 10)); + } } diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 5292fa32..a26986d3 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; import { UserStockRepository } from './user-stock.repository'; import { AssetRepository } from './asset.repository'; +import { MypageResponseDto } from './dto/mypage-response.dto'; +import { StockElementResponseDto } from './dto/stock-element-response.dto'; +import { AssetResponseDto } from './dto/asset-response.dto'; @Injectable() export class AssetService { @@ -23,4 +26,33 @@ export class AssetService { return { cash_balance: asset.cash_balance }; } + + async getMyPage(userId: number) { + const userStocks = + await this.userStockRepository.findUserStockWithNameByUserId(userId); + const asset = await this.assetRepository.findOneBy({ user_id: userId }); + + const myStocks = userStocks.map((userStock) => { + return new StockElementResponseDto( + userStock.stocks_name, + userStock.stocks_code, + userStock.user_stocks_quantity, + userStock.user_stocks_avg_price, + ); + }); + + const myAsset = new AssetResponseDto( + asset.cash_balance, + asset.stock_balance, + asset.total_asset, + asset.total_profit, + asset.total_profit_rate, + ); + + const response = new MypageResponseDto(); + response.asset = myAsset; + response.stocks = myStocks; + + return response; + } } diff --git a/BE/src/asset/dto/asset-response.dto.ts b/BE/src/asset/dto/asset-response.dto.ts new file mode 100644 index 00000000..8b855c4f --- /dev/null +++ b/BE/src/asset/dto/asset-response.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AssetResponseDto { + constructor( + cash_balance, + stock_balance, + total_asset, + total_profit, + total_profit_rate, + ) { + this.cash_balance = cash_balance; + this.stock_balance = stock_balance; + this.total_asset = total_asset; + this.total_profit = total_profit; + this.total_profit_rate = total_profit_rate; + } + + @ApiProperty({ description: '보유 현금' }) + cash_balance: number; + + @ApiProperty({ description: '주식 평가 금액' }) + stock_balance: number; + + @ApiProperty({ description: '총 자산' }) + total_asset: number; + + @ApiProperty({ description: '총 수익금' }) + total_profit: number; + + @ApiProperty({ description: '총 수익률' }) + total_profit_rate: number; +} diff --git a/BE/src/asset/dto/mypage-response.dto.ts b/BE/src/asset/dto/mypage-response.dto.ts new file mode 100644 index 00000000..54e5873e --- /dev/null +++ b/BE/src/asset/dto/mypage-response.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StockElementResponseDto } from './stock-element-response.dto'; +import { AssetResponseDto } from './asset-response.dto'; + +export class MypageResponseDto { + @ApiProperty({ + description: '보유 자산', + type: AssetResponseDto, + }) + asset: AssetResponseDto; + + @ApiProperty({ + description: '보유 주식 리스트', + type: [StockElementResponseDto], + }) + stocks: StockElementResponseDto[]; +} diff --git a/BE/src/asset/dto/stock-element-response.dto.ts b/BE/src/asset/dto/stock-element-response.dto.ts new file mode 100644 index 00000000..d2ce6d27 --- /dev/null +++ b/BE/src/asset/dto/stock-element-response.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockElementResponseDto { + constructor(name, code, quantity, avg_price) { + this.name = name; + this.code = code; + this.quantity = quantity; + this.avg_price = avg_price; + } + + @ApiProperty({ description: '종목 이름' }) + name: string; + + @ApiProperty({ description: '종목 코드' }) + code: string; + + @ApiProperty({ description: '보유량' }) + quantity: number; + + @ApiProperty({ description: '평균 매수가' }) + avg_price: number; +} diff --git a/BE/src/asset/interface/user-stock.interface.ts b/BE/src/asset/interface/user-stock.interface.ts new file mode 100644 index 00000000..44734a50 --- /dev/null +++ b/BE/src/asset/interface/user-stock.interface.ts @@ -0,0 +1,11 @@ +export interface UserStockInterface { + user_stocks_id: number; + user_stocks_user_id: number; + user_stocks_stock_code: string; + user_stocks_quantity: number; + user_stocks_avg_price: string; + user_stocks_last_updated: Date; + stocks_code: string; + stocks_name: string; + stocks_market: string; +} diff --git a/BE/src/asset/user-stock.repository.ts b/BE/src/asset/user-stock.repository.ts index e945a667..817b759c 100644 --- a/BE/src/asset/user-stock.repository.ts +++ b/BE/src/asset/user-stock.repository.ts @@ -2,10 +2,22 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { UserStock } from './user-stock.entity'; +import { UserStockInterface } from './interface/user-stock.interface'; @Injectable() export class UserStockRepository extends Repository { constructor(@InjectDataSource() private dataSource: DataSource) { super(UserStock, dataSource.createEntityManager()); } + + findUserStockWithNameByUserId(userId: number) { + return this.createQueryBuilder('user_stocks') + .leftJoinAndSelect( + 'stocks', + 'stocks', + 'stocks.code = user_stocks.stock_code', + ) + .where('user_stocks.user_id = :userId', { userId }) + .getRawMany(); + } } From c7aa9ab7274760d1d1487311dbd559594ff64aa0 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 16:41:05 +0900 Subject: [PATCH 42/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20credential=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/src/main.ts b/BE/src/main.ts index 3a48167d..83042aa6 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -20,6 +20,7 @@ async function bootstrap() { methods: 'GET, HEAD, PUT, PATH, POST, DELETE', preflightContinue: false, optionsSuccessStatus: 204, + credentials: true, }); app.use(cookieParser()); From 52dad5148c8ccbc97232f20303df190cfe29e538 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 17:01:11 +0900 Subject: [PATCH 43/81] =?UTF-8?q?=E2=9C=A8=20feat=20:=20ranking=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=84=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20=EB=9E=AD=ED=82=B9=20=EC=9A=94=EC=B2=AD=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/common/redis/redis.domain-service.ts | 4 +++ BE/src/ranking/ranking.controller.ts | 16 +++++++-- BE/src/ranking/ranking.entity.ts | 9 +++-- BE/src/ranking/ranking.module.ts | 8 ++++- BE/src/ranking/ranking.repository.ts | 39 +++++++++++++++++++++ BE/src/ranking/ranking.service.ts | 26 +++++++++++++- 6 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 BE/src/ranking/ranking.repository.ts diff --git a/BE/src/common/redis/redis.domain-service.ts b/BE/src/common/redis/redis.domain-service.ts index ce636d6e..b3c68809 100644 --- a/BE/src/common/redis/redis.domain-service.ts +++ b/BE/src/common/redis/redis.domain-service.ts @@ -8,6 +8,10 @@ export class RedisDomainService { private readonly redis: Redis, ) {} + async exists(key: string): Promise { + return this.redis.exists(key); + } + async get(key: string): Promise { return this.redis.get(key); } diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index 5775064f..969f27e8 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -1,4 +1,14 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; +import { RankingService } from './ranking.service'; +import { ApiOperation } from '@nestjs/swagger'; -@Controller('ranking') -export class RankingController {} +@Controller('/api/ranking') +export class RankingController { + constructor(private readonly rankingService: RankingService) {} + + @ApiOperation({ summary: '랭킹 조회' }) + @Get() + async getRanking() { + return this.rankingService.getRanking(); + } +} diff --git a/BE/src/ranking/ranking.entity.ts b/BE/src/ranking/ranking.entity.ts index ed771d74..a1753589 100644 --- a/BE/src/ranking/ranking.entity.ts +++ b/BE/src/ranking/ranking.entity.ts @@ -3,16 +3,18 @@ import { BaseEntity, Column, Entity, + JoinColumn, ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; @Entity() -export class Rankings extends BaseEntity { +export class Ranking extends BaseEntity { @PrimaryGeneratedColumn() id: number; - @ManyToOne(() => User, (user) => user.id) + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) user: User; @Column() @@ -20,7 +22,4 @@ export class Rankings extends BaseEntity { @Column() profitRate: number; - - @Column() - period: number; } diff --git a/BE/src/ranking/ranking.module.ts b/BE/src/ranking/ranking.module.ts index d89566b8..62f69691 100644 --- a/BE/src/ranking/ranking.module.ts +++ b/BE/src/ranking/ranking.module.ts @@ -5,10 +5,16 @@ import { Asset } from 'src/asset/asset.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RedisModule } from 'src/common/redis/redis.module'; import { RedisDomainService } from 'src/common/redis/redis.domain-service'; +import { RankingRepository } from './ranking.repository'; @Module({ imports: [TypeOrmModule.forFeature([Asset]), RedisModule], controllers: [RankingController], - providers: [RankingService, RedisDomainService], + providers: [ + RankingService, + RedisDomainService, + RedisDomainService, + RankingRepository, + ], }) export class RankingModule {} diff --git a/BE/src/ranking/ranking.repository.ts b/BE/src/ranking/ranking.repository.ts new file mode 100644 index 00000000..a6313e58 --- /dev/null +++ b/BE/src/ranking/ranking.repository.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { Ranking } from './ranking.entity'; +import { DataSource, Repository } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; + +@Injectable() +export class RankingRepository extends Repository { + constructor(@InjectDataSource() private dataSource: DataSource) { + super(Ranking, dataSource.createEntityManager()); + } + + async getRanking() { + const ranking = await this.createQueryBuilder('ranking') + .select([ + 'ranking.id', + 'ranking.profit', + 'ranking.profitRate', + 'user.email', + ]) + .leftJoin('ranking.user', 'user') + .orderBy('ranking.profit', 'DESC') + .getMany(); + return ranking; + } + + async clearRanking() { + this.createQueryBuilder().delete().from(Ranking).execute; + } + + async setRanking(userId: number, profit: number, profitRate: number) { + const ranking = this.create({ + user: { id: userId }, + profit, + profitRate, + }); + + return await this.save(ranking); + } +} diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 6c92552c..5d45d2b3 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -1,4 +1,28 @@ import { Injectable } from '@nestjs/common'; +import { RankingRepository } from './ranking.repository'; +import { RedisDomainService } from 'src/common/redis/redis.domain-service'; +import { AssetRepository } from 'src/asset/asset.repository'; @Injectable() -export class RankingService {} +export class RankingService { + constructor( + private readonly rankingRepository: RankingRepository, + private readonly redisDomainService: RedisDomainService, + ) {} + async getRanking() { + const date = new Date().toISOString().slice(0, 10); + + const key = `ranking:${date}`; + if (await this.redisDomainService.exists(key)) { + return this.redisDomainService.zrevrange(key, 0, 99); + } + + const ranking = await this.rankingRepository.getRanking(); + + ranking.forEach((rank) => { + this.redisDomainService.zadd(key, rank.profit, rank.user.email); + }); + + return this.redisDomainService.zrevrange(key, 0, 99); + } +} From 5b0200ca1ee92871205c212d97682cb01af06ea0 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 17:54:58 +0900 Subject: [PATCH 44/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20accessToken?= =?UTF-8?q?=EB=8F=84=20=EC=BF=A0=ED=82=A4=EC=97=90=20=EB=8B=B4=EC=95=84?= =?UTF-8?q?=EC=84=9C=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 22e29cdb..5c244050 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -38,6 +38,7 @@ export class AuthController { const { accessToken, refreshToken } = await this.authService.loginUser(authCredentialsDto); + res.cookie('accessToken', accessToken, { httpOnly: true }); res.cookie('refreshToken', refreshToken, { httpOnly: true }); res.cookie('isRefreshToken', true, { httpOnly: true }); return res.status(200).json({ accessToken }); From 59d08668c0106fe435aa82b056c3338012ec7142 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 18:06:27 +0900 Subject: [PATCH 45/81] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=20=EC=9E=A5=20?= =?UTF-8?q?=EB=A7=88=EA=B0=90=20=EC=8B=9C=EA=B0=84=EB=A7=88=EB=8B=A4=20ran?= =?UTF-8?q?king=20=20table=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.repository.ts | 4 ++++ BE/src/ranking/ranking.entity.ts | 4 ++-- BE/src/ranking/ranking.repository.ts | 20 ++++++++++++-------- BE/src/ranking/ranking.service.ts | 23 ++++++++++++++++++++--- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/BE/src/asset/asset.repository.ts b/BE/src/asset/asset.repository.ts index cfc05c92..d8e0ad72 100644 --- a/BE/src/asset/asset.repository.ts +++ b/BE/src/asset/asset.repository.ts @@ -8,4 +8,8 @@ export class AssetRepository extends Repository { constructor(@InjectDataSource() dataSource: DataSource) { super(Asset, dataSource.createEntityManager()); } + + async getAssets() { + return await this.find(); + } } diff --git a/BE/src/ranking/ranking.entity.ts b/BE/src/ranking/ranking.entity.ts index a1753589..ad7e1ab8 100644 --- a/BE/src/ranking/ranking.entity.ts +++ b/BE/src/ranking/ranking.entity.ts @@ -18,8 +18,8 @@ export class Ranking extends BaseEntity { user: User; @Column() - profit: number; + profitRate: number; @Column() - profitRate: number; + asset: number; } diff --git a/BE/src/ranking/ranking.repository.ts b/BE/src/ranking/ranking.repository.ts index a6313e58..bf6adc1e 100644 --- a/BE/src/ranking/ranking.repository.ts +++ b/BE/src/ranking/ranking.repository.ts @@ -18,7 +18,7 @@ export class RankingRepository extends Repository { 'user.email', ]) .leftJoin('ranking.user', 'user') - .orderBy('ranking.profit', 'DESC') + .orderBy('ranking.profitRate', 'DESC') .getMany(); return ranking; } @@ -27,13 +27,17 @@ export class RankingRepository extends Repository { this.createQueryBuilder().delete().from(Ranking).execute; } - async setRanking(userId: number, profit: number, profitRate: number) { - const ranking = this.create({ - user: { id: userId }, - profit, - profitRate, - }); + async setRanking( + rankingData: { userId: number; profit: number; profitRate: number }[], + ) { + const rankings = this.create( + rankingData.map((data) => ({ + user: { id: data.userId }, + profit: data.profit, + profitRate: data.profitRate, + })), + ); - return await this.save(ranking); + return await this.save(rankings); } } diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 5d45d2b3..8568a794 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -2,11 +2,13 @@ import { Injectable } from '@nestjs/common'; import { RankingRepository } from './ranking.repository'; import { RedisDomainService } from 'src/common/redis/redis.domain-service'; import { AssetRepository } from 'src/asset/asset.repository'; +import { Cron } from '@nestjs/schedule'; @Injectable() export class RankingService { constructor( private readonly rankingRepository: RankingRepository, + private readonly assetRepository: AssetRepository, private readonly redisDomainService: RedisDomainService, ) {} async getRanking() { @@ -14,15 +16,30 @@ export class RankingService { const key = `ranking:${date}`; if (await this.redisDomainService.exists(key)) { - return this.redisDomainService.zrevrange(key, 0, 99); + return this.redisDomainService.zrevrange(key, 0, 9); } const ranking = await this.rankingRepository.getRanking(); ranking.forEach((rank) => { - this.redisDomainService.zadd(key, rank.profit, rank.user.email); + this.redisDomainService.zadd(key, rank.profitRate, rank.user.email); }); - return this.redisDomainService.zrevrange(key, 0, 99); + return this.redisDomainService.zrevrange(key, 0, 9); + } + + @Cron('0 35 3 * * *') + async updateRanking() { + const assets = await this.assetRepository.getAssets(); + const ranking = assets + .map((asset) => ({ + userId: asset.user_id, + profit: asset.total_profit, + profitRate: asset.total_profit_rate, + })) + .sort((a, b) => b.profitRate - a.profitRate); + + await this.rankingRepository.clearRanking(); + await this.rankingRepository.setRanking(ranking); } } From df10b0ec9a8d4aee29d59352ed6c745c64fc5d76 Mon Sep 17 00:00:00 2001 From: JIN <80706216+uuuo3o@users.noreply.github.com> Date: Tue, 19 Nov 2024 18:34:41 +0900 Subject: [PATCH 46/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20CORS=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=A3=BC=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/main.ts b/BE/src/main.ts index 83042aa6..90726725 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -17,7 +17,7 @@ async function bootstrap() { 'http://223.130.151.42:3000', 'http://juga.kro.kr', ], - methods: 'GET, HEAD, PUT, PATH, POST, DELETE', + methods: 'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS', preflightContinue: false, optionsSuccessStatus: 204, credentials: true, From 501b266ccc2f6a814f0222e3d961aa6eff3acb73 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 18:59:01 +0900 Subject: [PATCH 47/81] =?UTF-8?q?=E2=9C=A8=20feat=20:=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=90=9C=20=EC=9C=A0=EC=A0=80=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EC=9E=90=EC=8B=A0=EC=9D=98=20=EC=88=9C=EC=9C=84=EA=B9=8C?= =?UTF-8?q?=EC=A7=80=20=EB=B3=B4=EB=82=B4=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.repository.ts | 2 +- BE/src/auth/optional-auth-guard.ts | 10 +++++ BE/src/common/redis/redis.domain-service.ts | 4 ++ BE/src/ranking/ranking.controller.ts | 16 ++++++-- BE/src/ranking/ranking.module.ts | 6 ++- BE/src/ranking/ranking.repository.ts | 10 ++--- BE/src/ranking/ranking.service.ts | 45 ++++++++++++++++++--- 7 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 BE/src/auth/optional-auth-guard.ts diff --git a/BE/src/asset/asset.repository.ts b/BE/src/asset/asset.repository.ts index d8e0ad72..9c0693f8 100644 --- a/BE/src/asset/asset.repository.ts +++ b/BE/src/asset/asset.repository.ts @@ -10,6 +10,6 @@ export class AssetRepository extends Repository { } async getAssets() { - return await this.find(); + return this.find(); } } diff --git a/BE/src/auth/optional-auth-guard.ts b/BE/src/auth/optional-auth-guard.ts new file mode 100644 index 00000000..bd9fde59 --- /dev/null +++ b/BE/src/auth/optional-auth-guard.ts @@ -0,0 +1,10 @@ +import { AuthGuard } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { User } from './user.entity'; + +@Injectable() +export class OptionalAuthGuard extends AuthGuard('jwt') { + handleRequest(err: Error, user: TUser): TUser { + return user; + } +} diff --git a/BE/src/common/redis/redis.domain-service.ts b/BE/src/common/redis/redis.domain-service.ts index b3c68809..77700cc4 100644 --- a/BE/src/common/redis/redis.domain-service.ts +++ b/BE/src/common/redis/redis.domain-service.ts @@ -51,6 +51,10 @@ export class RedisDomainService { return this.redis.zrevrange(key, start, stop); } + async zrevrank(key: string, member: string): Promise { + return this.redis.zrevrank(key, member); + } + async zrem(key: string, member: string): Promise { return this.redis.zrem(key, member); } diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index 969f27e8..30435876 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -1,6 +1,8 @@ -import { Controller, Get } from '@nestjs/common'; -import { RankingService } from './ranking.service'; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { ApiOperation } from '@nestjs/swagger'; +import { Request } from 'express'; +import { OptionalAuthGuard } from 'src/auth/optional-auth-guard'; +import { RankingService } from './ranking.service'; @Controller('/api/ranking') export class RankingController { @@ -8,7 +10,13 @@ export class RankingController { @ApiOperation({ summary: '랭킹 조회' }) @Get() - async getRanking() { - return this.rankingService.getRanking(); + @UseGuards(OptionalAuthGuard) + async getRanking(@Req() req: Request) { + if (!req.user) { + return this.rankingService.getRanking(); + } + + const { email } = req.user; + return this.rankingService.getRankingAuthUser(email); } } diff --git a/BE/src/ranking/ranking.module.ts b/BE/src/ranking/ranking.module.ts index 62f69691..c44ac3cb 100644 --- a/BE/src/ranking/ranking.module.ts +++ b/BE/src/ranking/ranking.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; -import { RankingController } from './ranking.controller'; -import { RankingService } from './ranking.service'; import { Asset } from 'src/asset/asset.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RedisModule } from 'src/common/redis/redis.module'; import { RedisDomainService } from 'src/common/redis/redis.domain-service'; +import { AssetRepository } from 'src/asset/asset.repository'; import { RankingRepository } from './ranking.repository'; +import { RankingService } from './ranking.service'; +import { RankingController } from './ranking.controller'; @Module({ imports: [TypeOrmModule.forFeature([Asset]), RedisModule], @@ -15,6 +16,7 @@ import { RankingRepository } from './ranking.repository'; RedisDomainService, RedisDomainService, RankingRepository, + AssetRepository, ], }) export class RankingModule {} diff --git a/BE/src/ranking/ranking.repository.ts b/BE/src/ranking/ranking.repository.ts index bf6adc1e..83f9fc5e 100644 --- a/BE/src/ranking/ranking.repository.ts +++ b/BE/src/ranking/ranking.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { Ranking } from './ranking.entity'; import { DataSource, Repository } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; +import { Ranking } from './ranking.entity'; @Injectable() export class RankingRepository extends Repository { @@ -9,7 +9,7 @@ export class RankingRepository extends Repository { super(Ranking, dataSource.createEntityManager()); } - async getRanking() { + async getRanking(): Promise { const ranking = await this.createQueryBuilder('ranking') .select([ 'ranking.id', @@ -23,8 +23,8 @@ export class RankingRepository extends Repository { return ranking; } - async clearRanking() { - this.createQueryBuilder().delete().from(Ranking).execute; + async clearRanking(): Promise { + await this.createQueryBuilder().delete().from(Ranking).execute(); } async setRanking( @@ -38,6 +38,6 @@ export class RankingRepository extends Repository { })), ); - return await this.save(rankings); + return this.save(rankings); } } diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 8568a794..60bdc43a 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { RankingRepository } from './ranking.repository'; import { RedisDomainService } from 'src/common/redis/redis.domain-service'; import { AssetRepository } from 'src/asset/asset.repository'; import { Cron } from '@nestjs/schedule'; +import { RankingRepository } from './ranking.repository'; @Injectable() export class RankingService { @@ -11,21 +11,54 @@ export class RankingService { private readonly assetRepository: AssetRepository, private readonly redisDomainService: RedisDomainService, ) {} + async getRanking() { const date = new Date().toISOString().slice(0, 10); - const key = `ranking:${date}`; + if (await this.redisDomainService.exists(key)) { return this.redisDomainService.zrevrange(key, 0, 9); } const ranking = await this.rankingRepository.getRanking(); - ranking.forEach((rank) => { - this.redisDomainService.zadd(key, rank.profitRate, rank.user.email); - }); + await Promise.all( + ranking.map((rank) => + this.redisDomainService.zadd(key, rank.profitRate, rank.user.email), + ), + ); + + return { + topRank: await this.redisDomainService.zrevrange(key, 0, 9), + }; + } + + async getRankingAuthUser(email: string) { + const date = new Date().toISOString().slice(0, 10); + const key = `ranking:${date}`; + + if (await this.redisDomainService.exists(key)) { + const userRank = await this.redisDomainService.zrevrank(key, email); + return { + topRank: await this.redisDomainService.zrevrange(key, 0, 9), + userRank: userRank !== null ? userRank + 1 : null, + }; + } + + const ranking = await this.rankingRepository.getRanking(); + + await Promise.all( + ranking.map((rank) => + this.redisDomainService.zadd(key, rank.profitRate, rank.user.email), + ), + ); + + const userRank = await this.redisDomainService.zrevrank(key, email); - return this.redisDomainService.zrevrange(key, 0, 9); + return { + topRank: await this.redisDomainService.zrevrange(key, 0, 9), + userRank: userRank ? userRank + 1 : null, + }; } @Cron('0 35 3 * * *') From b338c10bbe5995262412f492e537a4decd994618 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 19:11:27 +0900 Subject: [PATCH 48/81] =?UTF-8?q?=F0=9F=93=9D=20docs=20:=20api=20swagger?= =?UTF-8?q?=20=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/dto/ranking-response.dto.ts | 13 +++++++++++++ BE/src/ranking/ranking.controller.ts | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 BE/src/ranking/dto/ranking-response.dto.ts diff --git a/BE/src/ranking/dto/ranking-response.dto.ts b/BE/src/ranking/dto/ranking-response.dto.ts new file mode 100644 index 00000000..b1a6ced7 --- /dev/null +++ b/BE/src/ranking/dto/ranking-response.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RankingResponseDto { + @ApiProperty({ + description: 'top 10 유저 랭킹', + }) + topRank: string[]; + + @ApiProperty({ + description: '로그인 한 유저의 랭킹', + }) + userRank?: number | null; +} diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index 30435876..d4f65449 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -1,14 +1,21 @@ import { Controller, Get, Req, UseGuards } from '@nestjs/common'; -import { ApiOperation } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; import { OptionalAuthGuard } from 'src/auth/optional-auth-guard'; import { RankingService } from './ranking.service'; +import { RankingResponseDto } from './dto/ranking-response.dto'; @Controller('/api/ranking') +@ApiTags('랭킹 API') export class RankingController { constructor(private readonly rankingService: RankingService) {} @ApiOperation({ summary: '랭킹 조회' }) + @ApiResponse({ + status: 200, + description: '랭킹 조회 성공', + type: RankingResponseDto, + }) @Get() @UseGuards(OptionalAuthGuard) async getRanking(@Req() req: Request) { From 83a6faa93e3b61f68fe807a0fd205aed58cb065a Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 19 Nov 2024 19:45:54 +0900 Subject: [PATCH 49/81] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=EB=90=9C=20=EC=BF=BC=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/enum/sort-type.enum.ts | 4 +++ BE/src/ranking/ranking.controller.ts | 20 ++++++++--- BE/src/ranking/ranking.entity.ts | 2 +- BE/src/ranking/ranking.repository.ts | 6 ++-- BE/src/ranking/ranking.service.ts | 49 +++++++++++++++++---------- 5 files changed, 54 insertions(+), 27 deletions(-) create mode 100644 BE/src/ranking/enum/sort-type.enum.ts diff --git a/BE/src/ranking/enum/sort-type.enum.ts b/BE/src/ranking/enum/sort-type.enum.ts new file mode 100644 index 00000000..758ac97c --- /dev/null +++ b/BE/src/ranking/enum/sort-type.enum.ts @@ -0,0 +1,4 @@ +export enum SortType { + PROFIT_RATE = 'profitRate', + ASSET = 'totalAsset', +} diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index d4f65449..cc8b7377 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, Req, UseGuards } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; import { OptionalAuthGuard } from 'src/auth/optional-auth-guard'; import { RankingService } from './ranking.service'; import { RankingResponseDto } from './dto/ranking-response.dto'; +import { SortType } from './enum/sort-type.enum'; @Controller('/api/ranking') @ApiTags('랭킹 API') @@ -18,12 +19,21 @@ export class RankingController { }) @Get() @UseGuards(OptionalAuthGuard) - async getRanking(@Req() req: Request) { + @ApiQuery({ + name: 'sortBy', + required: false, + description: 'profitRate: 수익률순, asset: 자산순', + enum: ['profitRate', 'asset'], + }) + async getRanking( + @Req() req: Request, + @Query('sortBy') sortBy: SortType = SortType.PROFIT_RATE, + ): Promise { if (!req.user) { - return this.rankingService.getRanking(); + return this.rankingService.getRanking(sortBy); } const { email } = req.user; - return this.rankingService.getRankingAuthUser(email); + return this.rankingService.getRankingAuthUser(email, sortBy); } } diff --git a/BE/src/ranking/ranking.entity.ts b/BE/src/ranking/ranking.entity.ts index ad7e1ab8..75ef7332 100644 --- a/BE/src/ranking/ranking.entity.ts +++ b/BE/src/ranking/ranking.entity.ts @@ -21,5 +21,5 @@ export class Ranking extends BaseEntity { profitRate: number; @Column() - asset: number; + totalAsset: number; } diff --git a/BE/src/ranking/ranking.repository.ts b/BE/src/ranking/ranking.repository.ts index 83f9fc5e..1fef4df7 100644 --- a/BE/src/ranking/ranking.repository.ts +++ b/BE/src/ranking/ranking.repository.ts @@ -9,16 +9,16 @@ export class RankingRepository extends Repository { super(Ranking, dataSource.createEntityManager()); } - async getRanking(): Promise { + async getRanking(sortBy): Promise { const ranking = await this.createQueryBuilder('ranking') .select([ 'ranking.id', - 'ranking.profit', + 'ranking.totalAsset', 'ranking.profitRate', 'user.email', ]) .leftJoin('ranking.user', 'user') - .orderBy('ranking.profitRate', 'DESC') + .orderBy(`ranking.${sortBy}`, 'DESC') .getMany(); return ranking; } diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 60bdc43a..7410ccb0 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -3,6 +3,8 @@ import { RedisDomainService } from 'src/common/redis/redis.domain-service'; import { AssetRepository } from 'src/asset/asset.repository'; import { Cron } from '@nestjs/schedule'; import { RankingRepository } from './ranking.repository'; +import { SortType } from './enum/sort-type.enum'; +import { Ranking } from './ranking.entity'; @Injectable() export class RankingService { @@ -12,44 +14,44 @@ export class RankingService { private readonly redisDomainService: RedisDomainService, ) {} - async getRanking() { + async getRanking(sortBy: SortType = SortType.PROFIT_RATE) { const date = new Date().toISOString().slice(0, 10); const key = `ranking:${date}`; - if (await this.redisDomainService.exists(key)) { - return this.redisDomainService.zrevrange(key, 0, 9); - } - - const ranking = await this.rankingRepository.getRanking(); + const ranking = await this.rankingRepository.getRanking(sortBy); await Promise.all( ranking.map((rank) => - this.redisDomainService.zadd(key, rank.profitRate, rank.user.email), + this.redisDomainService.zadd( + key, + this.getSortScore(rank, sortBy), + rank.user.email, + ), ), ); return { topRank: await this.redisDomainService.zrevrange(key, 0, 9), + userRank: null, }; } - async getRankingAuthUser(email: string) { + async getRankingAuthUser( + email: string, + sortBy: SortType = SortType.PROFIT_RATE, + ) { const date = new Date().toISOString().slice(0, 10); const key = `ranking:${date}`; - if (await this.redisDomainService.exists(key)) { - const userRank = await this.redisDomainService.zrevrank(key, email); - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: userRank !== null ? userRank + 1 : null, - }; - } - - const ranking = await this.rankingRepository.getRanking(); + const ranking = await this.rankingRepository.getRanking(sortBy); await Promise.all( ranking.map((rank) => - this.redisDomainService.zadd(key, rank.profitRate, rank.user.email), + this.redisDomainService.zadd( + key, + this.getSortScore(rank, sortBy), + rank.user.email, + ), ), ); @@ -75,4 +77,15 @@ export class RankingService { await this.rankingRepository.clearRanking(); await this.rankingRepository.setRanking(ranking); } + + private getSortScore(rank: Ranking, sortBy: SortType) { + switch (sortBy) { + case SortType.PROFIT_RATE: + return rank.profitRate; + case SortType.ASSET: + return rank.totalAsset; + default: + return rank.profitRate; + } + } } From fb1bde24c40164436abb4c1444be10aa90c8b2be Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 19 Nov 2024 19:50:38 +0900 Subject: [PATCH 50/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20cookie=20domain=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 5c244050..8cc1554d 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -38,9 +38,14 @@ export class AuthController { const { accessToken, refreshToken } = await this.authService.loginUser(authCredentialsDto); - res.cookie('accessToken', accessToken, { httpOnly: true }); - res.cookie('refreshToken', refreshToken, { httpOnly: true }); - res.cookie('isRefreshToken', true, { httpOnly: true }); + const cookieOptions = { + httpOnly: true, + domain: origin?.includes('localhost') ? 'localhost' : '223.130.151.42', + }; + + res.cookie('accessToken', accessToken, cookieOptions); + res.cookie('refreshToken', refreshToken, cookieOptions); + res.cookie('isRefreshToken', true, cookieOptions); return res.status(200).json({ accessToken }); } From f28773359d0822c90713087a09d921401fc6d34e Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 19 Nov 2024 20:11:53 +0900 Subject: [PATCH 51/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20cookie=20domain=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 8cc1554d..5c244050 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -38,14 +38,9 @@ export class AuthController { const { accessToken, refreshToken } = await this.authService.loginUser(authCredentialsDto); - const cookieOptions = { - httpOnly: true, - domain: origin?.includes('localhost') ? 'localhost' : '223.130.151.42', - }; - - res.cookie('accessToken', accessToken, cookieOptions); - res.cookie('refreshToken', refreshToken, cookieOptions); - res.cookie('isRefreshToken', true, cookieOptions); + res.cookie('accessToken', accessToken, { httpOnly: true }); + res.cookie('refreshToken', refreshToken, { httpOnly: true }); + res.cookie('isRefreshToken', true, { httpOnly: true }); return res.status(200).json({ accessToken }); } From 8b1de0cd5e4a348455d72d94879754422f2129ed Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 23:33:12 +0900 Subject: [PATCH 52/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D=20?= =?UTF-8?q?=ED=8F=89=EA=B0=80=20=EA=B0=80=EA=B2=A9=20=EB=B0=8F=20=EC=B4=9D?= =?UTF-8?q?=20=EC=9E=90=EC=82=B0=201=EB=B6=84=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#150?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 6 +++ BE/src/asset/asset.module.ts | 3 +- BE/src/asset/asset.service.ts | 45 ++++++++++++++++++++++ BE/src/asset/user-stock.repository.ts | 9 ++++- BE/src/stock/detail/stock-detail.module.ts | 1 + 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index fa07535c..09ab1b89 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -6,6 +6,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; +import { Cron } from '@nestjs/schedule'; import { JwtAuthGuard } from '../auth/jwt-auth-guard'; import { AssetService } from './asset.service'; import { MypageResponseDto } from './dto/mypage-response.dto'; @@ -67,4 +68,9 @@ export class AssetController { async getMyPage(@Req() request: Request) { return this.assetService.getMyPage(parseInt(request.user.userId, 10)); } + + @Cron('*/1 9-16 * * 1-5') + async updateStockBalance() { + await this.assetService.updateStockBalance(); + } } diff --git a/BE/src/asset/asset.module.ts b/BE/src/asset/asset.module.ts index 79f305fc..0b582ee1 100644 --- a/BE/src/asset/asset.module.ts +++ b/BE/src/asset/asset.module.ts @@ -6,9 +6,10 @@ import { AssetRepository } from './asset.repository'; 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'; @Module({ - imports: [TypeOrmModule.forFeature([Asset, UserStock])], + imports: [TypeOrmModule.forFeature([Asset, UserStock]), StockDetailModule], controllers: [AssetController], providers: [AssetService, AssetRepository, UserStockRepository], exports: [AssetRepository, UserStockRepository], diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index a26986d3..3f6b4d73 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -4,12 +4,15 @@ import { AssetRepository } from './asset.repository'; import { MypageResponseDto } from './dto/mypage-response.dto'; import { StockElementResponseDto } from './dto/stock-element-response.dto'; import { AssetResponseDto } from './dto/asset-response.dto'; +import { StockDetailService } from '../stock/detail/stock-detail.service'; +import { UserStock } from './user-stock.entity'; @Injectable() export class AssetService { constructor( private readonly userStockRepository: UserStockRepository, private readonly assetRepository: AssetRepository, + private readonly stockDetailService: StockDetailService, ) {} async getUserStockByCode(userId: number, stockCode: string) { @@ -55,4 +58,46 @@ export class AssetService { return response; } + + async updateStockBalance() { + const currPrices = await this.getCurrPrices(); + const assets = await this.assetRepository.find(); + + await Promise.allSettled( + assets.map(async (asset) => { + const userId = asset.user_id; + const userStocks = await this.userStockRepository.find({ + where: { user_id: userId }, + }); + + const totalPrice = userStocks.reduce( + (sum, userStock) => + sum + userStock.quantity * currPrices[userStock.stock_code], + 0, + ); + + await this.assetRepository.update(asset.id, { + stock_balance: totalPrice, + total_asset: asset.cash_balance + totalPrice, + }); + }), + ); + } + + private async getCurrPrices() { + const userStocks: UserStock[] = + await this.userStockRepository.findAllDistinctCode(); + const currPrices = {}; + + await Promise.allSettled( + userStocks.map(async (userStock) => { + const inquirePrice = await this.stockDetailService.getInquirePrice( + userStock.stock_code, + ); + currPrices[userStock.stock_code] = Number(inquirePrice.stck_prpr); + }), + ); + + return currPrices; + } } diff --git a/BE/src/asset/user-stock.repository.ts b/BE/src/asset/user-stock.repository.ts index 817b759c..6542047f 100644 --- a/BE/src/asset/user-stock.repository.ts +++ b/BE/src/asset/user-stock.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, MoreThan, Repository } from 'typeorm'; import { UserStock } from './user-stock.entity'; import { UserStockInterface } from './interface/user-stock.interface'; @@ -20,4 +20,11 @@ export class UserStockRepository extends Repository { .where('user_stocks.user_id = :userId', { userId }) .getRawMany(); } + + findAllDistinctCode() { + return this.createQueryBuilder('user_stocks') + .select('DISTINCT user_stocks.stock_code') + .where({ quantity: MoreThan(0) }) + .getRawMany(); + } } diff --git a/BE/src/stock/detail/stock-detail.module.ts b/BE/src/stock/detail/stock-detail.module.ts index 50f25a5e..67899209 100644 --- a/BE/src/stock/detail/stock-detail.module.ts +++ b/BE/src/stock/detail/stock-detail.module.ts @@ -10,5 +10,6 @@ import { Stocks } from './stock-detail.entity'; imports: [KoreaInvestmentModule, TypeOrmModule.forFeature([Stocks])], controllers: [StockDetailController], providers: [StockDetailService, StockDetailRepository], + exports: [StockDetailService], }) export class StockDetailModule {} From dbac7079e1a39a4456f65b68e1f2a6906d5b220b Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 19 Nov 2024 23:39:46 +0900 Subject: [PATCH 53/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20last=5Fupdated=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#150?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 3f6b4d73..d25cf82f 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -79,6 +79,7 @@ export class AssetService { await this.assetRepository.update(asset.id, { stock_balance: totalPrice, total_asset: asset.cash_balance + totalPrice, + last_updated: new Date(), }); }), ); From 830cbd9a70cf6731415e94589fa41562778fdb63 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 20 Nov 2024 11:53:52 +0900 Subject: [PATCH 54/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C?= =?UTF-8?q?=EC=97=90=EB=8F=84=20=EC=9E=90=EC=82=B0=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#150?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.service.ts | 53 ++++++++++++-------- BE/src/stock/order/stock-order.repository.ts | 2 - 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index d25cf82f..d35bcad9 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -6,6 +6,7 @@ import { StockElementResponseDto } from './dto/stock-element-response.dto'; import { AssetResponseDto } from './dto/asset-response.dto'; import { StockDetailService } from '../stock/detail/stock-detail.service'; import { UserStock } from './user-stock.entity'; +import { Asset } from './asset.entity'; @Injectable() export class AssetService { @@ -34,6 +35,10 @@ export class AssetService { const userStocks = await this.userStockRepository.findUserStockWithNameByUserId(userId); const asset = await this.assetRepository.findOneBy({ user_id: userId }); + const newAsset = await this.updateMyAsset( + asset, + await this.getCurrPrices(), + ); const myStocks = userStocks.map((userStock) => { return new StockElementResponseDto( @@ -45,11 +50,11 @@ export class AssetService { }); const myAsset = new AssetResponseDto( - asset.cash_balance, - asset.stock_balance, - asset.total_asset, - asset.total_profit, - asset.total_profit_rate, + newAsset.cash_balance, + newAsset.stock_balance, + newAsset.total_asset, + newAsset.total_profit, + newAsset.total_profit_rate, ); const response = new MypageResponseDto(); @@ -64,25 +69,29 @@ export class AssetService { const assets = await this.assetRepository.find(); await Promise.allSettled( - assets.map(async (asset) => { - const userId = asset.user_id; - const userStocks = await this.userStockRepository.find({ - where: { user_id: userId }, - }); - - const totalPrice = userStocks.reduce( - (sum, userStock) => - sum + userStock.quantity * currPrices[userStock.stock_code], - 0, - ); + assets.map((asset) => this.updateMyAsset(asset, currPrices)), + ); + } - await this.assetRepository.update(asset.id, { - stock_balance: totalPrice, - total_asset: asset.cash_balance + totalPrice, - last_updated: new Date(), - }); - }), + private async updateMyAsset(asset: Asset, currPrices) { + const userId = asset.user_id; + const userStocks = await this.userStockRepository.find({ + where: { user_id: userId }, + }); + + const totalPrice = userStocks.reduce( + (sum, userStock) => + sum + userStock.quantity * currPrices[userStock.stock_code], + 0, ); + + const updatedAsset = { + ...asset, + stock_balance: totalPrice, + total_asset: asset.cash_balance + totalPrice, + last_updated: new Date(), + }; + return this.assetRepository.save(updatedAsset); } private async getCurrPrices() { diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 559f06fa..a970495d 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -29,7 +29,6 @@ export class StockOrderRepository extends Repository { { id: order.id }, { status: StatusType.COMPLETE, completed_at: new Date() }, ); - // TODO: stock_balance와 total_asset은 실시간 주가에 따라 변동하도록 따로 구현해야 함 await queryRunner.manager .createQueryBuilder() .update(Asset) @@ -78,7 +77,6 @@ export class StockOrderRepository extends Repository { { id: order.id }, { status: StatusType.COMPLETE, completed_at: new Date() }, ); - // TODO: stock_balance와 total_asset은 실시간 주가에 따라 변동하도록 따로 구현해야 함 await queryRunner.manager .createQueryBuilder() .update(Asset) From 48a8abf69e2082dd0b350ba6186cfc0f3c682644 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 20 Nov 2024 13:22:37 +0900 Subject: [PATCH 55/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=AA=85=EC=9D=84=20=ED=99=95=EC=8B=A4?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD#160?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/common/websocket/socket.gateway.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BE/src/common/websocket/socket.gateway.ts b/BE/src/common/websocket/socket.gateway.ts index 32ff997d..48668008 100644 --- a/BE/src/common/websocket/socket.gateway.ts +++ b/BE/src/common/websocket/socket.gateway.ts @@ -21,7 +21,7 @@ export class SocketGateway { this.server.emit(event, stockIndexValue); } - sendStockTradeHistoryValueToClient(event, chartData) { - this.server.emit(event, chartData); + sendStockTradeHistoryValueToClient(event, historyData) { + this.server.emit(event, historyData); } } From e4d8a2b3915b903e843733453ba9a7016a1095f5 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 20 Nov 2024 13:23:13 +0900 Subject: [PATCH 56/81] =?UTF-8?q?=E2=9C=A8=20feat:=20socket.io=EB=A1=9C?= =?UTF-8?q?=EB=8F=84=20=EC=A0=84=EC=86=A1=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84#160?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/history/stock-trade-history-socket.service.ts | 9 ++++++++- .../trade/history/stock-trade-history.controller.ts | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) 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 index dab0aea2..3c0f8fa4 100644 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts @@ -6,6 +6,7 @@ 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'; @Injectable() export class StockTradeHistorySocketService implements OnModuleInit { @@ -16,6 +17,8 @@ export class StockTradeHistorySocketService implements OnModuleInit { 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); @@ -52,10 +55,14 @@ export class StockTradeHistorySocketService implements OnModuleInit { this.eventSubject.next({ data: JSON.stringify({ - stockCode: data[1], tradeData, }), }); + + this.socketGateway.sendStockTradeHistoryValueToClient( + `trade-history/${dataList[0]}`, + tradeData, + ); }; this.socket.onclose = () => { 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 cbbe09c1..a41f584a 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -30,7 +30,12 @@ export class StockTradeHistoryController { type: TodayStockTradeHistoryDataDto, }) getTodayStockTradeHistory(@Param('stockCode') stockCode: string) { - return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); + const data = + this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); + + this.stockTradeHistorySocketService.subscribeByCode(stockCode); + + return data; } @Get(':stockCode/daily') From 3bebc30102771977d5fea9417261907514c40185 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 20 Nov 2024 13:24:33 +0900 Subject: [PATCH 57/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80=20#15?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.module.ts | 12 ++++++++-- BE/src/auth/dto/profile-response.dto.ts | 9 ++++++++ BE/src/auth/user.controller.ts | 30 +++++++++++++++++++++++++ BE/src/auth/user.entity.ts | 3 +++ BE/src/auth/user.service.ts | 13 +++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 BE/src/auth/dto/profile-response.dto.ts create mode 100644 BE/src/auth/user.controller.ts create mode 100644 BE/src/auth/user.service.ts diff --git a/BE/src/auth/auth.module.ts b/BE/src/auth/auth.module.ts index 581b401c..2b6834a5 100644 --- a/BE/src/auth/auth.module.ts +++ b/BE/src/auth/auth.module.ts @@ -10,6 +10,8 @@ import { UserRepository } from './user.repository'; import { JwtStrategy } from './strategy/jwt.strategy'; import { KakaoStrategy } from './strategy/kakao.strategy'; import { AssetModule } from '../asset/asset.module'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; @Module({ imports: [ @@ -28,8 +30,14 @@ import { AssetModule } from '../asset/asset.module'; }), AssetModule, ], - controllers: [AuthController], - providers: [AuthService, UserRepository, JwtStrategy, KakaoStrategy], + controllers: [AuthController, UserController], + providers: [ + AuthService, + UserRepository, + JwtStrategy, + KakaoStrategy, + UserService, + ], exports: [JwtStrategy, PassportModule], }) export class AuthModule {} diff --git a/BE/src/auth/dto/profile-response.dto.ts b/BE/src/auth/dto/profile-response.dto.ts new file mode 100644 index 00000000..e134876d --- /dev/null +++ b/BE/src/auth/dto/profile-response.dto.ts @@ -0,0 +1,9 @@ +export class ProfileResponseDto { + constructor(name, email) { + this.name = name; + this.email = email; + } + + name: string; + email: string; +} diff --git a/BE/src/auth/user.controller.ts b/BE/src/auth/user.controller.ts new file mode 100644 index 00000000..881a2b7c --- /dev/null +++ b/BE/src/auth/user.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import { Request } from 'express'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { UserService } from './user.service'; +import { JwtAuthGuard } from './jwt-auth-guard'; +import { ProfileResponseDto } from './dto/profile-response.dto'; + +@Controller('/api/user') +@ApiTags('프로필 API') +export class UserController { + constructor(private readonly userService: UserService) {} + + @Get('/profile') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: '내 프로필 조회 API' }) + @ApiResponse({ + status: 200, + description: '프로필 조회 성공', + type: ProfileResponseDto, + }) + getProfile(@Req() request: Request) { + return this.userService.getProfile(parseInt(request.user.userId, 10)); + } +} diff --git a/BE/src/auth/user.entity.ts b/BE/src/auth/user.entity.ts index 16773177..ac5dbbfa 100644 --- a/BE/src/auth/user.entity.ts +++ b/BE/src/auth/user.entity.ts @@ -6,6 +6,9 @@ export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; + @Column() + name: string; + @Column() email: string; diff --git a/BE/src/auth/user.service.ts b/BE/src/auth/user.service.ts new file mode 100644 index 00000000..05dfc89e --- /dev/null +++ b/BE/src/auth/user.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { UserRepository } from './user.repository'; +import { ProfileResponseDto } from './dto/profile-response.dto'; + +@Injectable() +export class UserService { + constructor(private readonly userRepository: UserRepository) {} + + async getProfile(userId: number) { + const user = await this.userRepository.findOneBy({ id: userId }); + return new ProfileResponseDto(user.name, user.email); + } +} From bf640146a3ecedf7e6fdcf5ecad0d43083b427ec Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 20 Nov 2024 14:06:16 +0900 Subject: [PATCH 58/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EB=B2=97=EC=96=B4=EB=82=A0=20=EB=95=8C=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=EC=9D=84=20=EC=B7=A8=EC=86=8C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20api=20=EA=B5=AC=ED=98=84#160?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../history/stock-trade-history.controller.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 a41f584a..74f21c44 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -85,4 +85,21 @@ export class StockTradeHistoryController { }; }); } + + @Get(':stockCode/unsubscribe') + @ApiOperation({ summary: '페이지를 벗어날 때 구독을 취소하기 위한 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: '구독 취소 성공', + }) + unsubscribeCode(@Param('stockCode') stockCode: string) { + this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); + } } From 0ef3369122fcc127cfa3cf302705079287286fd3 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 20 Nov 2024 15:36:53 +0900 Subject: [PATCH 59/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=9E=90=EC=82=B0?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=A3=BC=EA=B8=B0=20?= =?UTF-8?q?10=EB=B6=84=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20#150?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이페이지 조회 시 업데이트하도록 수정해서 1분마다 또 업데이트 할 필요가 없어졌기 때문에 주기 10분으로 수정함. --- BE/src/asset/asset.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index 09ab1b89..43264a63 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -69,7 +69,7 @@ export class AssetController { return this.assetService.getMyPage(parseInt(request.user.userId, 10)); } - @Cron('*/1 9-16 * * 1-5') + @Cron('*/10 9-16 * * 1-5') async updateStockBalance() { await this.assetService.updateStockBalance(); } From 20facee9f76e0ff3ce8a04d51d5efc58bfab22b7 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 20 Nov 2024 15:54:44 +0900 Subject: [PATCH 60/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EC=A3=BC=EC=8B=9D=EA=B0=80=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=ED=95=B4=EC=A3=BC=EB=8A=94=20=EC=8B=A4?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=86=8C=EC=BC=93=20=EA=B5=AC=ED=98=84#97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../history/dto/stock-detail-socket-data.dto.ts | 15 +++++++++++++++ .../history/stock-trade-history-socket.service.ts | 13 +++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 BE/src/stock/trade/history/dto/stock-detail-socket-data.dto.ts diff --git a/BE/src/stock/trade/history/dto/stock-detail-socket-data.dto.ts b/BE/src/stock/trade/history/dto/stock-detail-socket-data.dto.ts new file mode 100644 index 00000000..c13362d9 --- /dev/null +++ b/BE/src/stock/trade/history/dto/stock-detail-socket-data.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockDetailSocketDataDto { + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; +} 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 index 3c0f8fa4..9b055a3c 100644 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts @@ -7,6 +7,7 @@ import { SocketConnectTokenInterface } from '../../../common/websocket/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 { @@ -53,6 +54,13 @@ export class StockTradeHistorySocketService implements OnModuleInit { 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, @@ -63,6 +71,11 @@ export class StockTradeHistorySocketService implements OnModuleInit { `trade-history/${dataList[0]}`, tradeData, ); + + this.socketGateway.sendStockIndexValueToClient( + `detail/${dataList[0]}`, + detailData, + ); }; this.socket.onclose = () => { From 8eec6d5c17c3a098932ddd64a1069b689a5acaa3 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 16:19:38 +0900 Subject: [PATCH 61/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20redis?= =?UTF-8?q?Domainservice=20=EB=91=90=EB=B2=88=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=ED=95=98=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=20=EC=88=98=EC=A0=95(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/BE/src/ranking/ranking.module.ts b/BE/src/ranking/ranking.module.ts index c44ac3cb..1f2e03d2 100644 --- a/BE/src/ranking/ranking.module.ts +++ b/BE/src/ranking/ranking.module.ts @@ -14,7 +14,6 @@ import { RankingController } from './ranking.controller'; providers: [ RankingService, RedisDomainService, - RedisDomainService, RankingRepository, AssetRepository, ], From 5b2ec0f2f7c8cfc1a5f098686e667ed3c1bfa5cd Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 16:25:00 +0900 Subject: [PATCH 62/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20redis=EC=97=90?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=80=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20redis=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 7410ccb0..7f8695b1 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -16,7 +16,14 @@ export class RankingService { async getRanking(sortBy: SortType = SortType.PROFIT_RATE) { const date = new Date().toISOString().slice(0, 10); - const key = `ranking:${date}`; + const key = `ranking:${date}:${sortBy}`; + + if (await this.redisDomainService.exists(key)) { + return { + topRank: await this.redisDomainService.zrevrange(key, 0, 9), + userRank: null, + }; + } const ranking = await this.rankingRepository.getRanking(sortBy); @@ -41,7 +48,18 @@ export class RankingService { sortBy: SortType = SortType.PROFIT_RATE, ) { const date = new Date().toISOString().slice(0, 10); - const key = `ranking:${date}`; + const key = `ranking:${date}:${sortBy}`; + + let userRank = null; + + if (await this.redisDomainService.exists(key)) { + userRank = this.redisDomainService.zrevrank(key, email); + + return { + topRank: await this.redisDomainService.zrevrange(key, 0, 9), + userRank: userRank ? userRank + 1 : null, + }; + } const ranking = await this.rankingRepository.getRanking(sortBy); @@ -55,7 +73,7 @@ export class RankingService { ), ); - const userRank = await this.redisDomainService.zrevrank(key, email); + userRank = await this.redisDomainService.zrevrank(key, email); return { topRank: await this.redisDomainService.zrevrange(key, 0, 9), From da35e67f68e7e171546463e3a8c0c6eb92048a84 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 16:27:45 +0900 Subject: [PATCH 63/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20cron=20=EC=8B=9C?= =?UTF-8?q?=ED=96=89=20=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 7f8695b1..8bf3c6ea 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -81,7 +81,7 @@ export class RankingService { }; } - @Cron('0 35 3 * * *') + @Cron('0 16 * * 1-5') async updateRanking() { const assets = await this.assetRepository.getAssets(); const ranking = assets From f725a179173f7cdf6b20af6d62c6314785694b43 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 20 Nov 2024 16:40:12 +0900 Subject: [PATCH 64/81] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=AF=B8=EC=B2=B4?= =?UTF-8?q?=EA=B2=B0=EB=90=9C=20=EC=A3=BC=EB=AC=B8=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20#15?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/stock-order-element-response.dto.ts | 38 +++++++++++++++++++ .../order/interface/request.interface.ts | 9 ----- .../interface/stock-order-raw.interface.ts | 17 +++++++++ BE/src/stock/order/stock-order.controller.ts | 20 ++++++++++ BE/src/stock/order/stock-order.repository.ts | 8 ++++ BE/src/stock/order/stock-order.service.ts | 17 +++++++++ 6 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 BE/src/stock/order/dto/stock-order-element-response.dto.ts delete mode 100644 BE/src/stock/order/interface/request.interface.ts create mode 100644 BE/src/stock/order/interface/stock-order-raw.interface.ts diff --git a/BE/src/stock/order/dto/stock-order-element-response.dto.ts b/BE/src/stock/order/dto/stock-order-element-response.dto.ts new file mode 100644 index 00000000..8ea005ea --- /dev/null +++ b/BE/src/stock/order/dto/stock-order-element-response.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { TradeType } from '../enum/trade-type'; + +export class StockOrderElementResponseDto { + constructor( + stock_code: string, + stock_name: string, + amount: number, + price: number, + trade_type: TradeType, + created_at: Date, + ) { + this.stock_code = stock_code; + this.stock_name = stock_name; + this.amount = amount; + this.price = price; + this.trade_type = trade_type; + this.created_at = created_at; + } + + @ApiProperty({ description: '종목 코드' }) + stock_code: string; + + @ApiProperty({ description: '종목 이름' }) + stock_name: string; + + @ApiProperty({ description: '매도/매수 희망 수량' }) + amount: number; + + @ApiProperty({ description: '매도/매수 희망 가격' }) + price: number; + + @ApiProperty({ description: '매도: SELL, 매수: BUY' }) + trade_type: TradeType; + + @ApiProperty({ description: '주문 시간' }) + created_at: Date; +} diff --git a/BE/src/stock/order/interface/request.interface.ts b/BE/src/stock/order/interface/request.interface.ts deleted file mode 100644 index d7616da0..00000000 --- a/BE/src/stock/order/interface/request.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface RequestInterface { - user: { - id: number; - email: string; - password: string; - tutorial: boolean; - kakaoId: number; - }; -} diff --git a/BE/src/stock/order/interface/stock-order-raw.interface.ts b/BE/src/stock/order/interface/stock-order-raw.interface.ts new file mode 100644 index 00000000..293048c6 --- /dev/null +++ b/BE/src/stock/order/interface/stock-order-raw.interface.ts @@ -0,0 +1,17 @@ +import { StatusType } from '../enum/status-type'; +import { TradeType } from '../enum/trade-type'; + +export interface StockOrderRawInterface { + o_id: number; + o_user_id: number; + o_stock_code: string; + o_trade_type: TradeType; + o_amount: number; + o_price: number; + o_status: StatusType; + o_created_at: Date; + o_completed_at: Date; + s_code: string; + s_name: string; + s_market: string; +} diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index 268f4786..aee1ea34 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, + Get, Param, Post, Req, @@ -18,6 +19,7 @@ import { Request } from 'express'; import { StockOrderService } from './stock-order.service'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { JwtAuthGuard } from '../../auth/jwt-auth-guard'; +import { StockOrderElementResponseDto } from './dto/stock-order-element-response.dto'; @Controller('/api/stocks/trade') @ApiTags('주식 매수/매도 API') @@ -83,4 +85,22 @@ export class StockOrderController { orderId, ); } + + @Get('/list') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '미체결 주문 리스트 조회 API', + description: '미체결 주문 취소를 위해, 미체결된 주문 리스트를 조회한다.', + }) + @ApiResponse({ + status: 200, + description: '미체결 주문 리스트 조회 성공', + type: [StockOrderElementResponseDto], + }) + async getPendingList(@Req() request: Request) { + return this.stockOrderService.getPendingListByUserId( + parseInt(request.user.userId, 10), + ); + } } diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index a970495d..568af5c4 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -5,6 +5,7 @@ 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() export class StockOrderRepository extends Repository { @@ -109,4 +110,11 @@ export class StockOrderRepository extends Repository { await queryRunner.release(); } } + + async findAllPendingOrdersByUserId(userId: number) { + return this.createQueryBuilder('o') + .leftJoinAndSelect('stocks', 's', 's.code = o.stock_code') + .where({ user_id: userId, status: StatusType.PENDING }) + .getRawMany(); + } } diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 1f4e8a58..f8fa6dac 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -12,6 +12,7 @@ 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'; @Injectable() export class StockOrderService { @@ -87,4 +88,20 @@ export class StockOrderService { ) this.stockOrderSocketService.unsubscribeByCode(order.stock_code); } + + async getPendingListByUserId(userId: number) { + const stockOrderRaws = + await this.stockOrderRepository.findAllPendingOrdersByUserId(userId); + + return stockOrderRaws.map((stockOrderRaw) => { + return new StockOrderElementResponseDto( + stockOrderRaw.o_stock_code, + stockOrderRaw.s_name, + stockOrderRaw.o_amount, + stockOrderRaw.o_price, + stockOrderRaw.o_trade_type, + stockOrderRaw.o_created_at, + ); + }); + } } From a792b8dab319d81a8f6dc0376d52a9de07f63521 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 16:56:18 +0900 Subject: [PATCH 65/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20asset=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20=EC=96=B4=EC=A0=9C=20?= =?UTF-8?q?=EC=9E=90=20total=5Fasset=20=EC=A0=80=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.entity.ts | 3 +++ BE/src/asset/asset.service.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/BE/src/asset/asset.entity.ts b/BE/src/asset/asset.entity.ts index b3504463..57082cd7 100644 --- a/BE/src/asset/asset.entity.ts +++ b/BE/src/asset/asset.entity.ts @@ -27,4 +27,7 @@ export class Asset { @Column({ nullable: true }) last_updated?: Date; + + @Column({ nullable: true }) + prev_total_asset?: number; } diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index d35bcad9..5a273be4 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -90,6 +90,7 @@ export class AssetService { stock_balance: totalPrice, total_asset: asset.cash_balance + totalPrice, last_updated: new Date(), + prev_total_asset: asset.total_asset, }; return this.assetRepository.save(updatedAsset); } From 47e10ddffadc4943dbe072506b72d20335b65f6c Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 17:10:07 +0900 Subject: [PATCH 66/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20prev=5Ftotal=5Fa?= =?UTF-8?q?sset=20=EC=BB=AC=EB=9F=BC=20default=20=EA=B0=92=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/asset/asset.entity.ts b/BE/src/asset/asset.entity.ts index 57082cd7..f729263e 100644 --- a/BE/src/asset/asset.entity.ts +++ b/BE/src/asset/asset.entity.ts @@ -28,6 +28,6 @@ export class Asset { @Column({ nullable: true }) last_updated?: Date; - @Column({ nullable: true }) + @Column({ default: INIT_ASSET }) prev_total_asset?: number; } From a68044c0546dbf6decb63ebb0c7cdfcc02170b67 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 17:45:42 +0900 Subject: [PATCH 67/81] =?UTF-8?q?=E2=9C=A8=20feat=20:=20profitRate=20?= =?UTF-8?q?=EC=9D=BC=EA=B0=84=20=20=EC=88=98=EC=9D=B5=EB=A5=A0=20=EB=A1=9C?= =?UTF-8?q?=20=EA=B3=84=EC=82=B0=20=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EB=B0=8F=20ranking=20nickname=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/strategy/jwt.strategy.ts | 2 ++ BE/src/auth/user.entity.ts | 3 +++ BE/src/ranking/ranking.controller.ts | 8 ++++---- BE/src/ranking/ranking.repository.ts | 6 +++--- BE/src/ranking/ranking.service.ts | 19 +++++++++++-------- BE/src/types/express.d.ts | 1 + 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/BE/src/auth/strategy/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts index 049e2cf2..a638aba3 100644 --- a/BE/src/auth/strategy/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -38,12 +38,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) { email: string; tutorial: boolean; kakaoId: string | null; + nickname: string; }> { const { email } = payload; const user: User = await this.userRepository.findOne({ where: { email } }); if (!user) throw new UnauthorizedException(); return { + nickname: user.nickname, userId: user.id, email: user.email, tutorial: user.tutorial, diff --git a/BE/src/auth/user.entity.ts b/BE/src/auth/user.entity.ts index ac5dbbfa..fa31448c 100644 --- a/BE/src/auth/user.entity.ts +++ b/BE/src/auth/user.entity.ts @@ -15,6 +15,9 @@ export class User extends BaseEntity { @Column() password: string; + @Column({ default: '' }) + nickname: string; + @Column({ default: false }) tutorial: boolean; diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index cc8b7377..3681e957 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -22,8 +22,8 @@ export class RankingController { @ApiQuery({ name: 'sortBy', required: false, - description: 'profitRate: 수익률순, asset: 자산순', - enum: ['profitRate', 'asset'], + description: 'profitRate: 수익률순, totalAsset: 자산순', + enum: ['profitRate', 'totalAsset'], }) async getRanking( @Req() req: Request, @@ -33,7 +33,7 @@ export class RankingController { return this.rankingService.getRanking(sortBy); } - const { email } = req.user; - return this.rankingService.getRankingAuthUser(email, sortBy); + const { nickname } = req.user; + return this.rankingService.getRankingAuthUser(nickname, sortBy); } } diff --git a/BE/src/ranking/ranking.repository.ts b/BE/src/ranking/ranking.repository.ts index 1fef4df7..339246e8 100644 --- a/BE/src/ranking/ranking.repository.ts +++ b/BE/src/ranking/ranking.repository.ts @@ -15,7 +15,7 @@ export class RankingRepository extends Repository { 'ranking.id', 'ranking.totalAsset', 'ranking.profitRate', - 'user.email', + 'user.nickname', ]) .leftJoin('ranking.user', 'user') .orderBy(`ranking.${sortBy}`, 'DESC') @@ -28,12 +28,12 @@ export class RankingRepository extends Repository { } async setRanking( - rankingData: { userId: number; profit: number; profitRate: number }[], + rankingData: { userId: number; totalAsset: number; profitRate: number }[], ) { const rankings = this.create( rankingData.map((data) => ({ user: { id: data.userId }, - profit: data.profit, + totalAsset: data.totalAsset, profitRate: data.profitRate, })), ); diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 8bf3c6ea..96485053 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -32,7 +32,7 @@ export class RankingService { this.redisDomainService.zadd( key, this.getSortScore(rank, sortBy), - rank.user.email, + rank.user.nickname, ), ), ); @@ -44,7 +44,7 @@ export class RankingService { } async getRankingAuthUser( - email: string, + nickname: string, sortBy: SortType = SortType.PROFIT_RATE, ) { const date = new Date().toISOString().slice(0, 10); @@ -53,11 +53,11 @@ export class RankingService { let userRank = null; if (await this.redisDomainService.exists(key)) { - userRank = this.redisDomainService.zrevrank(key, email); + userRank = await this.redisDomainService.zrevrank(key, nickname); return { topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: userRank ? userRank + 1 : null, + userRank: userRank !== null ? userRank + 1 : null, }; } @@ -68,12 +68,12 @@ export class RankingService { this.redisDomainService.zadd( key, this.getSortScore(rank, sortBy), - rank.user.email, + rank.user.nickname, ), ), ); - userRank = await this.redisDomainService.zrevrank(key, email); + userRank = await this.redisDomainService.zrevrank(key, nickname); return { topRank: await this.redisDomainService.zrevrange(key, 0, 9), @@ -87,8 +87,11 @@ export class RankingService { const ranking = assets .map((asset) => ({ userId: asset.user_id, - profit: asset.total_profit, - profitRate: asset.total_profit_rate, + totalAsset: asset.total_asset, + profitRate: + ((asset.total_asset - asset.prev_total_asset) / + asset.prev_total_asset) * + 100, })) .sort((a, b) => b.profitRate - a.profitRate); diff --git a/BE/src/types/express.d.ts b/BE/src/types/express.d.ts index 2060c079..0ad1af45 100644 --- a/BE/src/types/express.d.ts +++ b/BE/src/types/express.d.ts @@ -7,6 +7,7 @@ declare module 'express' { kakaoId?: string; userId?: UUID; email?: string; + nickname?: string; }; } } From c743ad6fff7beecea88d7afc1196b52f2b3e982d Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 18:23:29 +0900 Subject: [PATCH 68/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20mysql=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EB=B6=80=EB=B6=84=20redis=EB=A1=9C=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.repository.ts | 5 +- BE/src/ranking/interface/ranking.interface.ts | 6 ++ BE/src/ranking/ranking.entity.ts | 25 -------- BE/src/ranking/ranking.module.ts | 8 +-- BE/src/ranking/ranking.repository.ts | 43 ------------- BE/src/ranking/ranking.service.ts | 61 +++++++++++++++---- 6 files changed, 61 insertions(+), 87 deletions(-) create mode 100644 BE/src/ranking/interface/ranking.interface.ts delete mode 100644 BE/src/ranking/ranking.entity.ts delete mode 100644 BE/src/ranking/ranking.repository.ts diff --git a/BE/src/asset/asset.repository.ts b/BE/src/asset/asset.repository.ts index 9c0693f8..3aac066b 100644 --- a/BE/src/asset/asset.repository.ts +++ b/BE/src/asset/asset.repository.ts @@ -10,6 +10,9 @@ export class AssetRepository extends Repository { } async getAssets() { - return this.find(); + return this.createQueryBuilder('asset') + .leftJoin('user', 'user', 'asset.user_id = user.id') + .select(['asset.* ', 'user.nickname as nickname']) + .getRawMany(); } } diff --git a/BE/src/ranking/interface/ranking.interface.ts b/BE/src/ranking/interface/ranking.interface.ts new file mode 100644 index 00000000..4ec58225 --- /dev/null +++ b/BE/src/ranking/interface/ranking.interface.ts @@ -0,0 +1,6 @@ +export interface Ranking { + id: number; + totalAsset: number; + profitRate: number; + nickname: string; +} diff --git a/BE/src/ranking/ranking.entity.ts b/BE/src/ranking/ranking.entity.ts deleted file mode 100644 index 75ef7332..00000000 --- a/BE/src/ranking/ranking.entity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { User } from 'src/auth/user.entity'; -import { - BaseEntity, - Column, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; - -@Entity() -export class Ranking extends BaseEntity { - @PrimaryGeneratedColumn() - id: number; - - @ManyToOne(() => User, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'userId' }) - user: User; - - @Column() - profitRate: number; - - @Column() - totalAsset: number; -} diff --git a/BE/src/ranking/ranking.module.ts b/BE/src/ranking/ranking.module.ts index 1f2e03d2..0ef57dd7 100644 --- a/BE/src/ranking/ranking.module.ts +++ b/BE/src/ranking/ranking.module.ts @@ -4,18 +4,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { RedisModule } from 'src/common/redis/redis.module'; import { RedisDomainService } from 'src/common/redis/redis.domain-service'; import { AssetRepository } from 'src/asset/asset.repository'; -import { RankingRepository } from './ranking.repository'; import { RankingService } from './ranking.service'; import { RankingController } from './ranking.controller'; @Module({ imports: [TypeOrmModule.forFeature([Asset]), RedisModule], controllers: [RankingController], - providers: [ - RankingService, - RedisDomainService, - RankingRepository, - AssetRepository, - ], + providers: [RankingService, RedisDomainService, AssetRepository], }) export class RankingModule {} diff --git a/BE/src/ranking/ranking.repository.ts b/BE/src/ranking/ranking.repository.ts deleted file mode 100644 index 339246e8..00000000 --- a/BE/src/ranking/ranking.repository.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; -import { InjectDataSource } from '@nestjs/typeorm'; -import { Ranking } from './ranking.entity'; - -@Injectable() -export class RankingRepository extends Repository { - constructor(@InjectDataSource() private dataSource: DataSource) { - super(Ranking, dataSource.createEntityManager()); - } - - async getRanking(sortBy): Promise { - const ranking = await this.createQueryBuilder('ranking') - .select([ - 'ranking.id', - 'ranking.totalAsset', - 'ranking.profitRate', - 'user.nickname', - ]) - .leftJoin('ranking.user', 'user') - .orderBy(`ranking.${sortBy}`, 'DESC') - .getMany(); - return ranking; - } - - async clearRanking(): Promise { - await this.createQueryBuilder().delete().from(Ranking).execute(); - } - - async setRanking( - rankingData: { userId: number; totalAsset: number; profitRate: number }[], - ) { - const rankings = this.create( - rankingData.map((data) => ({ - user: { id: data.userId }, - totalAsset: data.totalAsset, - profitRate: data.profitRate, - })), - ); - - return this.save(rankings); - } -} diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 96485053..26901dd9 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -2,14 +2,12 @@ import { Injectable } from '@nestjs/common'; import { RedisDomainService } from 'src/common/redis/redis.domain-service'; import { AssetRepository } from 'src/asset/asset.repository'; import { Cron } from '@nestjs/schedule'; -import { RankingRepository } from './ranking.repository'; import { SortType } from './enum/sort-type.enum'; -import { Ranking } from './ranking.entity'; +import { Ranking } from './interface/ranking.interface'; @Injectable() export class RankingService { constructor( - private readonly rankingRepository: RankingRepository, private readonly assetRepository: AssetRepository, private readonly redisDomainService: RedisDomainService, ) {} @@ -25,14 +23,14 @@ export class RankingService { }; } - const ranking = await this.rankingRepository.getRanking(sortBy); + const ranking = await this.calculateRanking(sortBy); await Promise.all( ranking.map((rank) => this.redisDomainService.zadd( key, this.getSortScore(rank, sortBy), - rank.user.nickname, + rank.nickname, ), ), ); @@ -61,14 +59,13 @@ export class RankingService { }; } - const ranking = await this.rankingRepository.getRanking(sortBy); - + const ranking = await this.calculateRanking(sortBy); await Promise.all( ranking.map((rank) => this.redisDomainService.zadd( key, this.getSortScore(rank, sortBy), - rank.user.nickname, + rank.nickname, ), ), ); @@ -83,20 +80,62 @@ export class RankingService { @Cron('0 16 * * 1-5') async updateRanking() { + const [profitRateRanking, assetRanking] = await Promise.all([ + this.calculateRanking(SortType.PROFIT_RATE), + this.calculateRanking(SortType.ASSET), + ]); + + const date = new Date().toISOString().slice(0, 10); + const profitRateKey = `ranking:${date}:${SortType.PROFIT_RATE}`; + const assetKey = `ranking:${date}:${SortType.ASSET}`; + + await Promise.all([ + this.redisDomainService.del(profitRateKey), + this.redisDomainService.del(assetKey), + ]); + + await Promise.all([ + Promise.all( + profitRateRanking.map((rank) => + this.redisDomainService.zadd( + profitRateKey, + rank.profitRate, + rank.nickname, + ), + ), + ), + Promise.all( + assetRanking.map((rank) => + this.redisDomainService.zadd( + assetKey, + rank.totalAsset, + rank.nickname, + ), + ), + ), + ]); + } + + async calculateRanking(sortBy: SortType) { const assets = await this.assetRepository.getAssets(); const ranking = assets .map((asset) => ({ + id: asset.id, userId: asset.user_id, + nickname: asset.nickname, totalAsset: asset.total_asset, profitRate: ((asset.total_asset - asset.prev_total_asset) / asset.prev_total_asset) * 100, })) - .sort((a, b) => b.profitRate - a.profitRate); + .sort((a, b) => + sortBy === 'profitRate' + ? b.profitRate - a.profitRate + : b.totalAsset - a.totalAsset, + ); - await this.rankingRepository.clearRanking(); - await this.rankingRepository.setRanking(ranking); + return ranking; } private getSortScore(rank: Ranking, sortBy: SortType) { From 69efe27dbaa8048dba8da690ccb82467c3449262 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 20 Nov 2024 18:27:56 +0900 Subject: [PATCH 69/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20lint?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 26901dd9..ed4e5577 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -26,7 +26,7 @@ export class RankingService { const ranking = await this.calculateRanking(sortBy); await Promise.all( - ranking.map((rank) => + ranking.map((rank: Ranking) => this.redisDomainService.zadd( key, this.getSortScore(rank, sortBy), @@ -61,7 +61,7 @@ export class RankingService { const ranking = await this.calculateRanking(sortBy); await Promise.all( - ranking.map((rank) => + ranking.map((rank: Ranking) => this.redisDomainService.zadd( key, this.getSortScore(rank, sortBy), @@ -96,7 +96,7 @@ export class RankingService { await Promise.all([ Promise.all( - profitRateRanking.map((rank) => + profitRateRanking.map((rank: Ranking) => this.redisDomainService.zadd( profitRateKey, rank.profitRate, @@ -105,7 +105,7 @@ export class RankingService { ), ), Promise.all( - assetRanking.map((rank) => + assetRanking.map((rank: Ranking) => this.redisDomainService.zadd( assetKey, rank.totalAsset, @@ -130,7 +130,7 @@ export class RankingService { 100, })) .sort((a, b) => - sortBy === 'profitRate' + sortBy === SortType.PROFIT_RATE ? b.profitRate - a.profitRate : b.totalAsset - a.totalAsset, ); From d82280f8300321405ac874d6ffbfccc3163ad62c Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 21 Nov 2024 11:08:51 +0900 Subject: [PATCH 70/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=AF=B8=EC=B2=B4?= =?UTF-8?q?=EA=B2=B0=20=EC=A3=BC=EB=AC=B8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=EA=B0=92=EC=97=90=20id=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/dto/stock-order-element-response.dto.ts | 5 +++++ BE/src/stock/order/stock-order.service.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/BE/src/stock/order/dto/stock-order-element-response.dto.ts b/BE/src/stock/order/dto/stock-order-element-response.dto.ts index 8ea005ea..dc5eb0e1 100644 --- a/BE/src/stock/order/dto/stock-order-element-response.dto.ts +++ b/BE/src/stock/order/dto/stock-order-element-response.dto.ts @@ -3,6 +3,7 @@ import { TradeType } from '../enum/trade-type'; export class StockOrderElementResponseDto { constructor( + id: number, stock_code: string, stock_name: string, amount: number, @@ -10,6 +11,7 @@ export class StockOrderElementResponseDto { trade_type: TradeType, created_at: Date, ) { + this.id = id; this.stock_code = stock_code; this.stock_name = stock_name; this.amount = amount; @@ -18,6 +20,9 @@ export class StockOrderElementResponseDto { this.created_at = created_at; } + @ApiProperty({ description: '주문 id' }) + id: number; + @ApiProperty({ description: '종목 코드' }) stock_code: string; diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index f8fa6dac..da9f6fcd 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -95,6 +95,7 @@ export class StockOrderService { return stockOrderRaws.map((stockOrderRaw) => { return new StockOrderElementResponseDto( + stockOrderRaw.o_id, stockOrderRaw.o_stock_code, stockOrderRaw.s_name, stockOrderRaw.o_amount, From 60cde1319c9a835eac3c73809cfd95dbd760b9c1 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 21 Nov 2024 11:30:09 +0900 Subject: [PATCH 71/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=88=98=EC=9D=B5?= =?UTF-8?q?=EA=B8=88=20=EC=88=98=EC=9D=B5=EB=A5=A0=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20flag=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메소드 이름도 변경 --- BE/src/asset/asset.controller.ts | 4 ++-- BE/src/asset/asset.service.ts | 5 ++++- BE/src/asset/dto/asset-response.dto.ts | 5 +++++ BE/src/stock/order/stock-order.repository.ts | 6 ------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index 43264a63..9596027c 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -70,7 +70,7 @@ export class AssetController { } @Cron('*/10 9-16 * * 1-5') - async updateStockBalance() { - await this.assetService.updateStockBalance(); + async updateAllAssets() { + await this.assetService.updateAllAssets(); } } diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 5a273be4..cf6c04db 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -55,6 +55,7 @@ export class AssetService { newAsset.total_asset, newAsset.total_profit, newAsset.total_profit_rate, + newAsset.total_profit_rate >= 0, ); const response = new MypageResponseDto(); @@ -64,7 +65,7 @@ export class AssetService { return response; } - async updateStockBalance() { + async updateAllAssets() { const currPrices = await this.getCurrPrices(); const assets = await this.assetRepository.find(); @@ -89,6 +90,8 @@ export class AssetService { ...asset, stock_balance: totalPrice, total_asset: asset.cash_balance + totalPrice, + total_profit: asset.cash_balance + totalPrice - 10000000, + total_profit_rate: (asset.cash_balance + totalPrice - 10000000) / 100000, last_updated: new Date(), prev_total_asset: asset.total_asset, }; diff --git a/BE/src/asset/dto/asset-response.dto.ts b/BE/src/asset/dto/asset-response.dto.ts index 8b855c4f..b24f5d6f 100644 --- a/BE/src/asset/dto/asset-response.dto.ts +++ b/BE/src/asset/dto/asset-response.dto.ts @@ -7,12 +7,14 @@ export class AssetResponseDto { total_asset, total_profit, total_profit_rate, + is_positive, ) { this.cash_balance = cash_balance; this.stock_balance = stock_balance; this.total_asset = total_asset; this.total_profit = total_profit; this.total_profit_rate = total_profit_rate; + this.is_positive = is_positive; } @ApiProperty({ description: '보유 현금' }) @@ -29,4 +31,7 @@ export class AssetResponseDto { @ApiProperty({ description: '총 수익률' }) total_profit_rate: number; + + @ApiProperty({ description: '수익률 부호' }) + is_positive: boolean; } diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 568af5c4..92da1ce6 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -35,9 +35,6 @@ export class StockOrderRepository extends Repository { .update(Asset) .set({ cash_balance: () => `cash_balance - :realPrice`, - total_asset: () => `total_asset - :realPrice`, - total_profit: () => `total_profit - :realPrice`, - total_profit_rate: () => `total_profit / 10000000`, last_updated: new Date(), }) .where({ user_id: order.user_id }) @@ -83,9 +80,6 @@ export class StockOrderRepository extends Repository { .update(Asset) .set({ cash_balance: () => `cash_balance + :realPrice`, - total_asset: () => `total_asset + :realPrice`, - total_profit: () => `total_profit + :realPrice`, - total_profit_rate: () => `total_profit / 10000000`, last_updated: new Date(), }) .where({ user_id: order.user_id }) From d0860e1db5d586f558f2d103e86d2ab9b487016f Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 21 Nov 2024 11:56:33 +0900 Subject: [PATCH 72/81] =?UTF-8?q?=E2=9E=95=20add:=20=EB=A7=A4=EC=9D=BC=206?= =?UTF-8?q?=EC=8B=9C=EC=97=90=20=EB=AF=B8=EC=B2=B4=EA=B2=B0=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=82=AD=EC=A0=9C=ED=95=98=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.controller.ts | 6 ++++++ BE/src/stock/order/stock-order.service.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index aee1ea34..0d5c5342 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -16,6 +16,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { Request } from 'express'; +import { Cron } from '@nestjs/schedule'; import { StockOrderService } from './stock-order.service'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { JwtAuthGuard } from '../../auth/jwt-auth-guard'; @@ -103,4 +104,9 @@ export class StockOrderController { parseInt(request.user.userId, 10), ); } + + @Cron('0 6 * * *') + async cronRemovePendingOrders() { + await this.stockOrderService.removePendingOrders(); + } } diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index da9f6fcd..81002143 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -13,6 +13,7 @@ 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'; @Injectable() export class StockOrderService { @@ -105,4 +106,17 @@ export class StockOrderService { ); }); } + + async removePendingOrders() { + const orders: Order[] = + await this.stockOrderRepository.findAllCodeByStatus(); + + await Promise.all( + orders.map((order) => + this.stockOrderSocketService.unsubscribeByCode(order.stock_code), + ), + ); + + await this.stockOrderRepository.delete({ status: StatusType.PENDING }); + } } From 13bf33c118c7dcf8cba1241d50813688fdf3d459 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 21 Nov 2024 13:03:56 +0900 Subject: [PATCH 73/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EB=A7=A4=EC=88=98/=EB=A7=A4=EB=8F=84=20API=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index 0d5c5342..6d309748 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -22,7 +22,7 @@ import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { JwtAuthGuard } from '../../auth/jwt-auth-guard'; import { StockOrderElementResponseDto } from './dto/stock-order-element-response.dto'; -@Controller('/api/stocks/trade') +@Controller('/api/stocks/order') @ApiTags('주식 매수/매도 API') export class StockOrderController { constructor(private readonly stockOrderService: StockOrderService) {} From 9890a4cf7d0e8d0ad831414d7913be4fb47439f0 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 21 Nov 2024 13:37:58 +0900 Subject: [PATCH 74/81] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=AF=B8=EC=B2=B4?= =?UTF-8?q?=EA=B2=B0=20=EC=A3=BC=EB=AC=B8=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index 6d309748..c43199d9 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -105,7 +105,7 @@ export class StockOrderController { ); } - @Cron('0 6 * * *') + @Cron('0 18 * * *') async cronRemovePendingOrders() { await this.stockOrderService.removePendingOrders(); } From d3cfc8474e66ca05663c11e85e9819abd4d6b2e1 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 21 Nov 2024 14:00:12 +0900 Subject: [PATCH 75/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20query=20?= =?UTF-8?q?=EB=B3=84=EB=A1=9C=20=EC=9D=91=EB=8B=B5=EC=9D=B4=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20=ED=95=9C=EB=B2=88=EC=9D=98=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=97=90=20=EB=91=90=EA=B0=9C=EC=9D=98=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20=EC=9D=91=EB=8B=B5=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/dto/ranking-data.dto.ts | 23 ++++ BE/src/ranking/dto/ranking-response.dto.ts | 9 +- BE/src/ranking/dto/ranking-result.dto.ts | 17 +++ .../interface/ranking-data.interface.ts | 5 + BE/src/ranking/ranking.controller.ts | 14 +- BE/src/ranking/ranking.service.ts | 120 ++++++++++-------- 6 files changed, 121 insertions(+), 67 deletions(-) create mode 100644 BE/src/ranking/dto/ranking-data.dto.ts create mode 100644 BE/src/ranking/dto/ranking-result.dto.ts create mode 100644 BE/src/ranking/interface/ranking-data.interface.ts diff --git a/BE/src/ranking/dto/ranking-data.dto.ts b/BE/src/ranking/dto/ranking-data.dto.ts new file mode 100644 index 00000000..97094da9 --- /dev/null +++ b/BE/src/ranking/dto/ranking-data.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RankingDataDto { + @ApiProperty({ + description: '사용자 닉네임', + example: 'trader123', + }) + nickname: string; + + @ApiProperty({ + description: '수익률 (%)', + example: 15.7, + required: false, + }) + profitRate?: number; + + @ApiProperty({ + description: '총 자산', + example: 1000000, + required: false, + }) + totalAsset?: number; +} diff --git a/BE/src/ranking/dto/ranking-response.dto.ts b/BE/src/ranking/dto/ranking-response.dto.ts index b1a6ced7..e89f53ce 100644 --- a/BE/src/ranking/dto/ranking-response.dto.ts +++ b/BE/src/ranking/dto/ranking-response.dto.ts @@ -1,13 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; +import { RankingResultDto } from './ranking-result.dto'; export class RankingResponseDto { @ApiProperty({ - description: 'top 10 유저 랭킹', + description: '수익률 랭킹', }) - topRank: string[]; + profitRateRanking: RankingResultDto; @ApiProperty({ - description: '로그인 한 유저의 랭킹', + description: '자산 랭킹', }) - userRank?: number | null; + assetRanking: RankingResultDto; } diff --git a/BE/src/ranking/dto/ranking-result.dto.ts b/BE/src/ranking/dto/ranking-result.dto.ts new file mode 100644 index 00000000..61fa5888 --- /dev/null +++ b/BE/src/ranking/dto/ranking-result.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { RankingDataDto } from './ranking-data.dto'; + +export class RankingResultDto { + @ApiProperty({ + description: '상위 10명의 랭킹 데이터', + type: [RankingDataDto], + }) + topRank: RankingDataDto[]; + + @ApiProperty({ + description: '현재 사용자의 순위 (없을 경우 null)', + example: 42, + nullable: true, + }) + userRank: number | null; +} diff --git a/BE/src/ranking/interface/ranking-data.interface.ts b/BE/src/ranking/interface/ranking-data.interface.ts new file mode 100644 index 00000000..098604b3 --- /dev/null +++ b/BE/src/ranking/interface/ranking-data.interface.ts @@ -0,0 +1,5 @@ +export interface RankingData { + nickname: string; + profitRate?: number; + totalAsset?: number; +} diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index 3681e957..420c9ebb 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -1,10 +1,9 @@ -import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; import { OptionalAuthGuard } from 'src/auth/optional-auth-guard'; import { RankingService } from './ranking.service'; import { RankingResponseDto } from './dto/ranking-response.dto'; -import { SortType } from './enum/sort-type.enum'; @Controller('/api/ranking') @ApiTags('랭킹 API') @@ -22,18 +21,13 @@ export class RankingController { @ApiQuery({ name: 'sortBy', required: false, - description: 'profitRate: 수익률순, totalAsset: 자산순', - enum: ['profitRate', 'totalAsset'], }) - async getRanking( - @Req() req: Request, - @Query('sortBy') sortBy: SortType = SortType.PROFIT_RATE, - ): Promise { + async getRanking(@Req() req: Request): Promise { if (!req.user) { - return this.rankingService.getRanking(sortBy); + return this.rankingService.getRanking(); } const { nickname } = req.user; - return this.rankingService.getRankingAuthUser(nickname, sortBy); + return this.rankingService.getRankingAuthUser(nickname); } } diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index ed4e5577..3eb58252 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -4,6 +4,9 @@ import { AssetRepository } from 'src/asset/asset.repository'; import { Cron } from '@nestjs/schedule'; import { SortType } from './enum/sort-type.enum'; import { Ranking } from './interface/ranking.interface'; +import { RankingResponseDto } from './dto/ranking-response.dto'; +import { RankingResultDto } from './dto/ranking-result.dto'; +import { RankingDataDto } from './dto/ranking-data.dto'; @Injectable() export class RankingService { @@ -12,69 +15,74 @@ export class RankingService { private readonly redisDomainService: RedisDomainService, ) {} - async getRanking(sortBy: SortType = SortType.PROFIT_RATE) { - const date = new Date().toISOString().slice(0, 10); - const key = `ranking:${date}:${sortBy}`; - - if (await this.redisDomainService.exists(key)) { - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: null, - }; - } - - const ranking = await this.calculateRanking(sortBy); + async getRanking(): Promise { + const profitRateRanking = await this.getRankingData(SortType.PROFIT_RATE); + const assetRanking = await this.getRankingData(SortType.ASSET); - await Promise.all( - ranking.map((rank: Ranking) => - this.redisDomainService.zadd( - key, - this.getSortScore(rank, sortBy), - rank.nickname, - ), - ), - ); + return { profitRateRanking, assetRanking }; + } - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: null, - }; + async getRankingAuthUser(nickname: string): Promise { + const profitRateRanking = await this.getRankingData(SortType.PROFIT_RATE, { + nickname, + }); + const assetRanking = await this.getRankingData(SortType.ASSET, { + nickname, + }); + return { profitRateRanking, assetRanking }; } - async getRankingAuthUser( - nickname: string, - sortBy: SortType = SortType.PROFIT_RATE, - ) { + private async getRankingData( + sortBy: SortType, + options: { nickname?: string } = { nickname: null }, + ): Promise { const date = new Date().toISOString().slice(0, 10); const key = `ranking:${date}:${sortBy}`; - let userRank = null; - - if (await this.redisDomainService.exists(key)) { - userRank = await this.redisDomainService.zrevrank(key, nickname); - - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: userRank !== null ? userRank + 1 : null, - }; + if (!(await this.redisDomainService.exists(key))) { + const ranking = await this.calculateRanking(sortBy); + await Promise.all( + ranking.map((rank: Ranking) => + this.redisDomainService.zadd( + key, + this.getSortScore(rank, sortBy), + sortBy === SortType.PROFIT_RATE + ? JSON.stringify({ + nickname: rank.nickname, + profitRate: rank.profitRate, + }) + : JSON.stringify({ + nickname: rank.nickname, + totalAsset: rank.totalAsset, + }), + ), + ), + ); } - const ranking = await this.calculateRanking(sortBy); - await Promise.all( - ranking.map((rank: Ranking) => - this.redisDomainService.zadd( - key, - this.getSortScore(rank, sortBy), - rank.nickname, - ), - ), - ); + const [topRank, userRank] = await Promise.all([ + this.redisDomainService.zrevrange(key, 0, 9), + options.nickname !== null + ? this.redisDomainService.zrevrank( + key, + JSON.stringify({ + nickname: options.nickname, + ...(sortBy === SortType.PROFIT_RATE + ? { profitRate: 0 } + : { totalAsset: 0 }), + }), + ) + : null, + ]); - userRank = await this.redisDomainService.zrevrank(key, nickname); + const parsedTopRank: RankingDataDto[] = topRank.map((rank) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + JSON.parse(rank), + ); return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: userRank ? userRank + 1 : null, + topRank: parsedTopRank, + userRank: userRank !== null ? userRank + 1 : null, }; } @@ -100,7 +108,10 @@ export class RankingService { this.redisDomainService.zadd( profitRateKey, rank.profitRate, - rank.nickname, + JSON.stringify({ + nickname: rank.nickname, + profitRate: rank.profitRate, + }), ), ), ), @@ -109,7 +120,10 @@ export class RankingService { this.redisDomainService.zadd( assetKey, rank.totalAsset, - rank.nickname, + JSON.stringify({ + nickname: rank.nickname, + totalAsset: rank.totalAsset, + }), ), ), ), From ff380a63350e0ab27f72134f9366975d42eabd19 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 21 Nov 2024 14:29:42 +0900 Subject: [PATCH 76/81] =?UTF-8?q?=F0=9F=94=A5=20remove=20:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20interface=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/interface/ranking-data.interface.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 BE/src/ranking/interface/ranking-data.interface.ts diff --git a/BE/src/ranking/interface/ranking-data.interface.ts b/BE/src/ranking/interface/ranking-data.interface.ts deleted file mode 100644 index 098604b3..00000000 --- a/BE/src/ranking/interface/ranking-data.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RankingData { - nickname: string; - profitRate?: number; - totalAsset?: number; -} From a15a636a87aa5dd15ad297cc88b4ed9c70da107b Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 21 Nov 2024 14:54:48 +0900 Subject: [PATCH 77/81] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20userRanking=20?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index 3eb58252..dc8e6b96 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -60,19 +60,23 @@ export class RankingService { ); } + const findUserRank = async () => { + if (!options.nickname) return null; + + const members = await this.redisDomainService.zrange(key, 0, -1); + const userMember = members.find((member) => { + const parsed = JSON.parse(member); + return parsed.nickname === options.nickname; + }); + + return userMember + ? this.redisDomainService.zrevrank(key, userMember) + : null; + }; + const [topRank, userRank] = await Promise.all([ this.redisDomainService.zrevrange(key, 0, 9), - options.nickname !== null - ? this.redisDomainService.zrevrank( - key, - JSON.stringify({ - nickname: options.nickname, - ...(sortBy === SortType.PROFIT_RATE - ? { profitRate: 0 } - : { totalAsset: 0 }), - }), - ) - : null, + findUserRank(), ]); const parsedTopRank: RankingDataDto[] = topRank.map((rank) => From 374199073885806578c8ad57482f9614556bf7dd Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 21 Nov 2024 14:58:30 +0900 Subject: [PATCH 78/81] =?UTF-8?q?=F0=9F=93=9D=20docs=20:=20swagger=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=B4=EC=A7=84=20query=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=88=98=EC=A0=95(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.controller.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index 420c9ebb..e3f48d11 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Req, UseGuards } from '@nestjs/common'; -import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; import { OptionalAuthGuard } from 'src/auth/optional-auth-guard'; import { RankingService } from './ranking.service'; @@ -18,10 +18,6 @@ export class RankingController { }) @Get() @UseGuards(OptionalAuthGuard) - @ApiQuery({ - name: 'sortBy', - required: false, - }) async getRanking(@Req() req: Request): Promise { if (!req.user) { return this.rankingService.getRanking(); From ccc4e4b66b3a038bd4a3d208bf2ddc94234b6572 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 21 Nov 2024 15:15:46 +0900 Subject: [PATCH 79/81] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=83=81=ED=83=9C=20=20=ED=99=95=EC=9D=B8=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 5c244050..bf462d29 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -80,4 +80,11 @@ export class AuthController { res.cookie('isRefreshToken', true, { httpOnly: true }); return res.redirect(this.configService.get('FRONTEND_URL')); } + + @ApiOperation({ summary: '로그인 상태 확인 API' }) + @Get('/check') + @UseGuards(AuthGuard('jwt')) + check() { + return { isLogin: true }; + } } From 7e7188bbd7b77147c5a344db1cdca40695e1c80c Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 21 Nov 2024 16:32:02 +0900 Subject: [PATCH 80/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20api=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 2 ++ BE/src/auth/auth.controller.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index 9596027c..360b85f4 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -26,6 +26,7 @@ export class AssetController { @ApiResponse({ status: 200, description: '매도 가능 주식 개수 조회 성공', + example: { quantity: 0 }, }) async getUserStockByCode( @Req() request: Request, @@ -48,6 +49,7 @@ export class AssetController { @ApiResponse({ status: 200, description: '매수 가능 금액 조회 성공', + example: { cash_balance: 0 }, }) async getCashBalance(@Req() request: Request) { return this.assetService.getCashBalance(parseInt(request.user.userId, 10)); diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index bf462d29..7e58a4bb 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -10,7 +10,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; -import { ApiOperation } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; @@ -84,6 +84,11 @@ export class AuthController { @ApiOperation({ summary: '로그인 상태 확인 API' }) @Get('/check') @UseGuards(AuthGuard('jwt')) + @ApiResponse({ + status: 200, + description: '로그인 상태 조회 성공', + example: { isLogin: true }, + }) check() { return { isLogin: true }; } From 0ff18f91b028716498bd4e407c967c41854410ca Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 21 Nov 2024 16:38:37 +0900 Subject: [PATCH 81/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20api=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EB=82=98=EB=A8=B8=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/dto/profile-response.dto.ts | 5 +++++ BE/src/stock/list/stock-list.controller.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/BE/src/auth/dto/profile-response.dto.ts b/BE/src/auth/dto/profile-response.dto.ts index e134876d..5363aaa8 100644 --- a/BE/src/auth/dto/profile-response.dto.ts +++ b/BE/src/auth/dto/profile-response.dto.ts @@ -1,9 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + export class ProfileResponseDto { constructor(name, email) { this.name = name; this.email = email; } + @ApiProperty({ description: '사용자 이름' }) name: string; + + @ApiProperty({ description: '사용자 이메일' }) email: string; } diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index 4db69192..e0ebb13c 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -22,6 +22,11 @@ export class StockListController { description: '모든 주식 종목 리스트를 조회한다.', }) @Get() + @ApiResponse({ + status: 200, + description: '주식 종목 리스트 조회 성공', + type: [StockListResponseDto], + }) async findAll(): Promise { return this.stockListService.findAll(); }