From 22b0e31a31e7fd4d6dcdbba5507003df04b33b88 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 27 Nov 2024 13:40:26 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B0=92=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EA=B2=8C=20=EA=B8=B0=EB=8A=A5=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/news/dto/news-data-output.dto.ts | 2 +- BE/src/news/dto/news-item-data.dto.ts | 18 +++++++++++++ BE/src/news/dto/news-response.dto.ts | 18 ++++--------- BE/src/news/news.controller.ts | 4 +-- BE/src/news/news.service.ts | 34 ++++++++++++++++++++++--- 5 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 BE/src/news/dto/news-item-data.dto.ts diff --git a/BE/src/news/dto/news-data-output.dto.ts b/BE/src/news/dto/news-data-output.dto.ts index d75c7886..74931d9f 100644 --- a/BE/src/news/dto/news-data-output.dto.ts +++ b/BE/src/news/dto/news-data-output.dto.ts @@ -3,5 +3,5 @@ export class NewsDataOutputDto { originallink: string; link: string; description: string; - pubDate: string; + pubDate: Date; } diff --git a/BE/src/news/dto/news-item-data.dto.ts b/BE/src/news/dto/news-item-data.dto.ts new file mode 100644 index 00000000..f5e1a2d5 --- /dev/null +++ b/BE/src/news/dto/news-item-data.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class NewsItemDataDto { + @ApiProperty({ description: '뉴스 기사 제목' }) + title: string; + + @ApiProperty({ description: '원문 URL' }) + originallink: string; + + @ApiProperty({ description: '뉴스 기사의 내용을 요약한 패시지 정보' }) + description: string; + + @ApiProperty({ description: '기사 원문이 제공된 시간' }) + pubDate: Date; + + @ApiProperty({ description: '검색 키워드' }) + query: string; +} diff --git a/BE/src/news/dto/news-response.dto.ts b/BE/src/news/dto/news-response.dto.ts index adc02bad..7707dd85 100644 --- a/BE/src/news/dto/news-response.dto.ts +++ b/BE/src/news/dto/news-response.dto.ts @@ -1,18 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; +import { NewsItemDataDto } from './news-item-data.dto'; export class NewsResponseDto { - @ApiProperty({ description: '뉴스 기사 제목' }) - title: string; + @ApiProperty({ description: '마지막 업데이트 시간' }) + updatedAt: Date; - @ApiProperty({ description: '원문 URL' }) - originallink: string; - - @ApiProperty({ description: '뉴스 기사의 내용을 요약한 패시지 정보' }) - description: string; - - @ApiProperty({ description: '기사 원문이 제공된 시간' }) - pubDate: string; - - @ApiProperty({ description: '검색 키워드' }) - query: string; + @ApiProperty({ description: '뉴스 목록', type: [NewsItemDataDto] }) + news: NewsItemDataDto[]; } diff --git a/BE/src/news/news.controller.ts b/BE/src/news/news.controller.ts index 70890e2b..81a910a6 100644 --- a/BE/src/news/news.controller.ts +++ b/BE/src/news/news.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get } from '@nestjs/common'; import { ApiResponse, ApiTags } from '@nestjs/swagger'; import { NewsService } from './news.service'; -import { NewsDatabaseResponseDto } from './dto/news-database-response.dto'; +import { NewsResponseDto } from './dto/news-response.dto'; @ApiTags('뉴스 API') @Controller('/api/news') @@ -12,7 +12,7 @@ export class NewsController { @ApiResponse({ status: 200, description: '뉴스 조회 성공', - type: [NewsDatabaseResponseDto], + type: [NewsResponseDto], }) async getNews() { return this.newsService.getNews(); diff --git a/BE/src/news/news.service.ts b/BE/src/news/news.service.ts index 79944c05..43cdda88 100644 --- a/BE/src/news/news.service.ts +++ b/BE/src/news/news.service.ts @@ -4,8 +4,10 @@ import { In } from 'typeorm'; import { NaverApiDomianService } from './naver-api-domian.service'; import { NewsApiResponse } from './interface/news-value.interface'; import { NewsDataOutputDto } from './dto/news-data-output.dto'; +import { NewsItemDataDto } from './dto/news-item-data.dto'; import { NewsResponseDto } from './dto/news-response.dto'; import { NewsRepository } from './news.repository'; +import { News } from './news.entity'; @Injectable() export class NewsService { @@ -14,8 +16,34 @@ export class NewsService { private readonly newsRepository: NewsRepository, ) {} - async getNews() { - return this.newsRepository.find(); + async getNews(): Promise { + const dbData: News[] = await this.newsRepository.find({ + select: { + title: true, + description: true, + pubDate: true, + originallink: true, + query: true, + updatedAt: true, + }, + order: { pubDate: 'DESC' }, + }); + + const updateTime = dbData[0].updatedAt; + const formattedNewsData = dbData.map( + ({ title, description, pubDate, originallink, query }) => ({ + title, + description, + pubDate, + originallink, + query, + }), + ); + + return { + updatedAt: updateTime, + news: formattedNewsData, + }; } @Cron('*/30 8-16 * * 1-5') @@ -46,7 +74,7 @@ export class NewsService { private formatNewsData(query: string, items: NewsDataOutputDto[]) { return items.slice(0, 10).map((item) => { - const result = new NewsResponseDto(); + const result = new NewsItemDataDto(); result.title = item.title.replace(/<\/?b>/g, ''); result.description = item.description.replace(/<\/?b>/g, ''); From 1ccd7eee52bf9634e741bbb15e7abac4b7cce26e Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 27 Nov 2024 14:40:04 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=94=A7=20fix:=20asset=20quantity=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=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.repository.ts | 7 ++++++- BE/src/asset/asset.service.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/BE/src/asset/asset.repository.ts b/BE/src/asset/asset.repository.ts index 71f22e99..a800b033 100644 --- a/BE/src/asset/asset.repository.ts +++ b/BE/src/asset/asset.repository.ts @@ -19,7 +19,11 @@ export class AssetRepository extends Repository { .getRawMany(); } - async findAllPendingOrders(userId: number, tradeType: TradeType) { + async findAllPendingOrders( + userId: number, + tradeType: TradeType, + stockCode?: string, + ) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.startTransaction(); @@ -29,6 +33,7 @@ export class AssetRepository extends Repository { user_id: userId, status: StatusType.PENDING, trade_type: tradeType, + ...(stockCode ? { stock_code: stockCode } : {}), }, }); diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index bb75e104..c081b694 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -29,6 +29,7 @@ export class AssetService { const pendingOrders = await this.assetRepository.findAllPendingOrders( userId, TradeType.SELL, + stockCode, ); const totalPendingCount = pendingOrders.reduce( (sum, pendingOrder) => sum + pendingOrder.amount, From c08e94188c9144cef1a6d21e912b72073e6715e7 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 27 Nov 2024 14:54:19 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=A8=20feat:=20html=20=ED=8A=B9?= =?UTF-8?q?=EC=88=98=20=EB=AC=B8=EC=9E=90=20=EB=B3=80=EA=B2=BD=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/news/news.service.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/BE/src/news/news.service.ts b/BE/src/news/news.service.ts index 43cdda88..4d05db80 100644 --- a/BE/src/news/news.service.ts +++ b/BE/src/news/news.service.ts @@ -46,7 +46,7 @@ export class NewsService { }; } - @Cron('*/30 8-16 * * 1-5') + @Cron('*/1 8-16 * * 1-5') async cronNewsData() { await this.newsRepository.delete({ query: In(['증권', '주식']) }); await this.getNewsDataByQuery('주식'); @@ -76,8 +76,8 @@ export class NewsService { return items.slice(0, 10).map((item) => { const result = new NewsItemDataDto(); - result.title = item.title.replace(/<\/?b>/g, ''); - result.description = item.description.replace(/<\/?b>/g, ''); + result.title = this.htmlEncode(item.title); + result.description = this.htmlEncode(item.description); result.originallink = item.originallink; result.pubDate = item.pubDate; result.query = query; @@ -85,4 +85,11 @@ export class NewsService { return result; }); } + + private htmlEncode(value: string) { + return value + .replace(/<\/?b>/g, '') + .replace(/"/g, '"') + .replace(/&/g, '&'); + } } From d91ccd89f89bd2c0ed2486d668facb323d8edc58 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 27 Nov 2024 15:28:55 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX:=20=EC=B2=B4?= =?UTF-8?q?=EA=B2=B0=EB=9F=89=EC=9D=B4=20=EB=A7=8E=EC=9D=80=20=EC=A2=85?= =?UTF-8?q?=EB=AA=A9=EC=97=90=20=EB=8C=80=ED=95=B4=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EC=B2=B4=EA=B2=B0=EC=9D=B4=20=EC=97=AC=EB=9F=AC?= =?UTF-8?q?=EB=B2=88=20=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock-execute-order.repository.ts | 204 ++++++++++++------ .../stockSocket/stock-price-socket.service.ts | 71 +----- 2 files changed, 144 insertions(+), 131 deletions(-) diff --git a/BE/src/stockSocket/stock-execute-order.repository.ts b/BE/src/stockSocket/stock-execute-order.repository.ts index 3a0eca92..5a993e4e 100644 --- a/BE/src/stockSocket/stock-execute-order.repository.ts +++ b/BE/src/stockSocket/stock-execute-order.repository.ts @@ -1,12 +1,21 @@ -import { DataSource, Repository } from 'typeorm'; +import { + DataSource, + LessThanOrEqual, + MoreThanOrEqual, + QueryRunner, + Repository, +} from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; -import { InternalServerErrorException } from '@nestjs/common'; +import { InternalServerErrorException, Logger } from '@nestjs/common'; import { Order } from '../stock/order/stock-order.entity'; import { StatusType } from '../stock/order/enum/status-type'; import { Asset } from '../asset/asset.entity'; import { UserStock } from '../asset/user-stock.entity'; +import { TradeType } from '../stock/order/enum/trade-type'; export class StockExecuteOrderRepository extends Repository { + private readonly logger = new Logger(); + constructor(@InjectDataSource() private dataSource: DataSource) { super(Order, dataSource.createEntityManager()); } @@ -18,43 +27,38 @@ export class StockExecuteOrderRepository extends Repository { .getRawMany(); } - async updateOrderAndAssetAndUserStockWhenBuy(order, realPrice) { + async checkExecutableOrder(stockCode, value) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.startTransaction(); try { - await queryRunner.manager.update( - Order, - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, + const buyOrders = await this.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.BUY, + status: StatusType.PENDING, + price: MoreThanOrEqual(value), + }, + }); + + const sellOrders = await this.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.SELL, + status: StatusType.PENDING, + price: LessThanOrEqual(value), + }, + }); + + await Promise.all( + buyOrders.map((buyOrder) => this.executeBuy(queryRunner, buyOrder)), ); - await queryRunner.manager - .createQueryBuilder() - .update(Asset) - .set({ - cash_balance: () => `cash_balance - :realPrice`, - last_updated: new Date(), - }) - .where({ user_id: order.user_id }) - .setParameters({ realPrice }) - .execute(); - - await queryRunner.query( - `INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE avg_price = (avg_price * quantity + ? * ?) / (quantity + ?), quantity = quantity + ?`, - [ - order.user_id, - order.stock_code, - order.amount, - order.price, - new Date(), - order.price, - order.amount, - order.amount, - order.amount, - ], + await Promise.all( + sellOrders.map((sellOrder) => this.executeSell(queryRunner, sellOrder)), ); await queryRunner.commitTransaction(); + return buyOrders.length + sellOrders.length; } catch (err) { await queryRunner.rollbackTransaction(); throw new InternalServerErrorException(err); @@ -63,43 +67,109 @@ export class StockExecuteOrderRepository extends Repository { } } - async updateOrderAndAssetAndUserStockWhenSell(order, realPrice) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction(); + private async executeBuy(queryRunner: QueryRunner, order) { + this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); - try { - await queryRunner.manager.update( - Order, - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, - ); - await queryRunner.manager - .createQueryBuilder() - .update(Asset) - .set({ - cash_balance: () => `cash_balance + :realPrice`, - last_updated: new Date(), - }) - .where({ user_id: order.user_id }) - .setParameters({ realPrice }) - .execute(); - - await queryRunner.manager - .createQueryBuilder() - .update(UserStock) - .set({ - quantity: () => `quantity - :newQuantity`, - }) - .where({ user_id: order.user_id, stock_code: order.stock_code }) - .setParameters({ newQuantity: order.amount }) - .execute(); + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.updateOrderAndAssetAndUserStockWhenBuy( + queryRunner, + order, + totalPrice + fee, + ); + } - await queryRunner.commitTransaction(); - } catch (err) { - await queryRunner.rollbackTransaction(); - throw new InternalServerErrorException(); - } finally { - await queryRunner.release(); - } + private async executeSell(queryRunner: QueryRunner, order) { + this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.updateOrderAndAssetAndUserStockWhenSell( + queryRunner, + order, + totalPrice - fee, + ); + } + + private async updateOrderAndAssetAndUserStockWhenBuy( + queryRunner: QueryRunner, + order, + realPrice, + ) { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => `cash_balance - :realPrice`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameters({ realPrice }) + .execute(); + + await queryRunner.query( + `INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE avg_price = (avg_price * quantity + ? * ?) / (quantity + ?), quantity = quantity + ?`, + [ + order.user_id, + order.stock_code, + order.amount, + order.price, + new Date(), + order.price, + order.amount, + order.amount, + order.amount, + ], + ); + } + + private async updateOrderAndAssetAndUserStockWhenSell( + queryRunner: QueryRunner, + order, + realPrice, + ) { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => `cash_balance + :realPrice`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameters({ realPrice }) + .execute(); + + await queryRunner.manager + .createQueryBuilder() + .update(UserStock) + .set({ + quantity: () => `quantity - :newQuantity`, + }) + .where({ user_id: order.user_id, stock_code: order.stock_code }) + .setParameters({ newQuantity: order.amount }) + .execute(); + + await queryRunner.commitTransaction(); + } + + private calculateFee(totalPrice: number) { + if (totalPrice <= 10000000) return Math.floor(totalPrice * 0.0016); + if (totalPrice > 10000000 && totalPrice <= 50000000) + return Math.floor(totalPrice * 0.0014); + if (totalPrice > 50000000 && totalPrice <= 100000000) + return Math.floor(totalPrice * 0.0012); + if (totalPrice > 100000000 && totalPrice <= 300000000) + return Math.floor(totalPrice * 0.001); + return Math.floor(totalPrice * 0.0008); } } diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts index 11cb9d65..40ea13d1 100644 --- a/BE/src/stockSocket/stock-price-socket.service.ts +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -1,15 +1,9 @@ -import { - Injectable, - InternalServerErrorException, - Logger, -} from '@nestjs/common'; -import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { filter, map, Observable, Subject } from 'rxjs'; import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; import { SocketGateway } from '../common/websocket/socket.gateway'; import { BaseStockSocketDomainService } from './base-stock-socket.domain-service'; import { Order } from '../stock/order/stock-order.entity'; -import { TradeType } from '../stock/order/enum/trade-type'; import { StatusType } from '../stock/order/enum/status-type'; import { TodayStockTradeHistoryDataDto } from '../stock/trade/history/dto/today-stock-trade-history-data.dto'; import { StockDetailSocketDataDto } from '../stock/trade/history/dto/stock-detail-socket-data.dto'; @@ -18,7 +12,6 @@ import { SseEvent } from '../stock/trade/history/interface/sse-event'; @Injectable() export class StockPriceSocketService extends BaseStockSocketDomainService { - private readonly logger = new Logger(); private connection: { [key: string]: number } = {}; private eventSubject = new Subject(); @@ -110,31 +103,14 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { } private async checkExecutableOrder(stockCode: string, value) { - const buyOrders = await this.stockExecuteOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.BUY, - status: StatusType.PENDING, - price: MoreThanOrEqual(value), - }, - }); - - const sellOrders = await this.stockExecuteOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.SELL, - status: StatusType.PENDING, - price: LessThanOrEqual(value), - }, - }); - - await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); - await Promise.all( - sellOrders.map((sellOrder) => this.executeSell(sellOrder)), - ); + const affectedRow = + await this.stockExecuteOrderRepository.checkExecutableOrder( + stockCode, + value, + ); if ( - buyOrders.length + sellOrders.length > 0 && + affectedRow > 0 && !(await this.stockExecuteOrderRepository.existsBy({ stock_code: stockCode, status: StatusType.PENDING, @@ -142,37 +118,4 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { ) this.unsubscribeByCode(stockCode); } - - private async executeBuy(order) { - this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockExecuteOrderRepository.updateOrderAndAssetAndUserStockWhenBuy( - order, - totalPrice + fee, - ); - } - - private async executeSell(order) { - this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockExecuteOrderRepository.updateOrderAndAssetAndUserStockWhenSell( - order, - totalPrice - fee, - ); - } - - private calculateFee(totalPrice: number) { - if (totalPrice <= 10000000) return Math.floor(totalPrice * 0.0016); - if (totalPrice > 10000000 && totalPrice <= 50000000) - return Math.floor(totalPrice * 0.0014); - if (totalPrice > 50000000 && totalPrice <= 100000000) - return Math.floor(totalPrice * 0.0012); - if (totalPrice > 100000000 && totalPrice <= 300000000) - return Math.floor(totalPrice * 0.001); - return Math.floor(totalPrice * 0.0008); - } } From 3ae07cf51876a8e9dfa04099156400b7eba2b46a Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 27 Nov 2024 15:43:37 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=94=A7=20fix:=20queryRunner=EC=97=90?= =?UTF-8?q?=EC=84=9C=20find=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stockSocket/stock-execute-order.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BE/src/stockSocket/stock-execute-order.repository.ts b/BE/src/stockSocket/stock-execute-order.repository.ts index 5a993e4e..af60a937 100644 --- a/BE/src/stockSocket/stock-execute-order.repository.ts +++ b/BE/src/stockSocket/stock-execute-order.repository.ts @@ -32,7 +32,7 @@ export class StockExecuteOrderRepository extends Repository { await queryRunner.startTransaction(); try { - const buyOrders = await this.find({ + const buyOrders = await queryRunner.manager.find(Order, { where: { stock_code: stockCode, trade_type: TradeType.BUY, @@ -41,7 +41,7 @@ export class StockExecuteOrderRepository extends Repository { }, }); - const sellOrders = await this.find({ + const sellOrders = await queryRunner.manager.find(Order, { where: { stock_code: stockCode, trade_type: TradeType.SELL,