diff --git a/packages/server/src/account/account.repository.ts b/packages/server/src/account/account.repository.ts index 452585fc..4138339f 100644 --- a/packages/server/src/account/account.repository.ts +++ b/packages/server/src/account/account.repository.ts @@ -77,6 +77,36 @@ export class AccountRepository extends Repository { throw error; } } + + async updateAccountCurrencyWithBid( + typeGiven: string, + change: number, + availableBalance: number, + accountId: number, + ){ + this.logger.log( + `계정 통화 업데이트 시작: accountId=${accountId}, type=${typeGiven}`, + ); + try { + await this + .createQueryBuilder() + .update(Account) + .set({ + // 직접 연산 처리: ()로 감싸 동적 SQL 계산 + [typeGiven]: () => `${typeGiven} + ${change}`, + availableKRW: () => `availableKRW + ${availableBalance}`, + }) + .where('id = :id', { id: accountId }) + .execute(); + this.logger.log(`계정 통화 업데이트 완료: accountId=${accountId}`); + } catch (error) { + this.logger.error( + `계정 통화 업데이트 실패: ${error.message}`, + error.stack, + ); + throw error; + } + } async updateAccountAvailableCurrency( change: number, accountId: number, @@ -173,7 +203,7 @@ export class AccountRepository extends Repository { } } - async getAccount(id: number, queryRunner: QueryRunner): Promise { + async getAccountWithQueryRunner(id: number, queryRunner: QueryRunner): Promise { this.logger.log(`계정 조회 시작: userId=${id}`); try { const account = await queryRunner.manager.findOne(Account, { @@ -187,4 +217,19 @@ export class AccountRepository extends Repository { throw error; } } + + async getAccount(id: number): Promise { + this.logger.log(`계정 조회 시작: userId=${id}`); + try { + const account = await this.findOne({ + where: { user: { id } }, + }); + + this.logger.log(`계정 조회 완료: userId=${id}`); + return account; + } catch (error) { + this.logger.error(`계정 조회 실패: ${error.message}`, error.stack); + throw error; + } + } } diff --git a/packages/server/src/asset/asset.repository.ts b/packages/server/src/asset/asset.repository.ts index 314e3ab4..7b7d8ffb 100644 --- a/packages/server/src/asset/asset.repository.ts +++ b/packages/server/src/asset/asset.repository.ts @@ -8,6 +8,7 @@ import { Account } from '@src/account/account.entity'; import { Asset } from './asset.entity'; import { Coin } from './dtos/asset.interface'; import { ensureMax8Decimals } from './utils/util'; +import { query } from 'express'; @Injectable() export class AssetRepository extends Repository { @@ -29,7 +30,6 @@ export class AssetRepository extends Repository { account: Account, price: number, quantity: number, - queryRunner: QueryRunner, ): Promise { this.logger.log(`자산 생성 시작: type=${typeReceived}, accountId=${account.id}`); try { @@ -44,7 +44,7 @@ export class AssetRepository extends Repository { asset.availableQuantity = quantity; asset.account = account; - const savedAsset = await queryRunner.manager.save(Asset, asset); + const savedAsset = await this.save(asset); this.logger.log(`자산 생성 완료: assetId=${savedAsset.assetId}`); return savedAsset; @@ -62,6 +62,39 @@ export class AssetRepository extends Repository { */ async updateAssetQuantityPrice( asset: Asset, + ): Promise { + this.logger.log(`자산 수량/가격 업데이트 시작: assetId=${asset.assetId}`); + try { + // --- [추가] 8자리 검사 --- + ensureMax8Decimals(asset.price, 'price'); + ensureMax8Decimals(asset.quantity, 'quantity'); + ensureMax8Decimals(asset.availableQuantity, 'availableQuantity'); + + await this + .createQueryBuilder() + .update(Asset) + .set({ + quantity: asset.quantity, + price: asset.price, + availableQuantity: asset.availableQuantity, + }) + .where('assetId = :assetId', { assetId: asset.assetId }) + .execute(); + + this.logger.log(`자산 수량/가격 업데이트 완료: assetId=${asset.assetId}`); + } catch (error) { + this.logger.error( + `자산 수량/가격 업데이트 실패: ${error.message}`, + error.stack, + ); + throw new InternalServerErrorException( + '자산 업데이트 중 오류가 발생했습니다.', + ); + } + } + + async updateAssetQuantityPriceWithQR( + asset: Asset, queryRunner: QueryRunner, ): Promise { this.logger.log(`자산 수량/가격 업데이트 시작: assetId=${asset.assetId}`); @@ -191,7 +224,7 @@ export class AssetRepository extends Repository { } } - async getAsset( + async getAssetWithQR( id: number, assetName: string, queryRunner: QueryRunner, @@ -217,6 +250,31 @@ export class AssetRepository extends Repository { } } + async getAsset( + id: number, + assetName: string, + ): Promise { + this.logger.log(`자산 조회 시작: accountId=${id}, assetName=${assetName}`); + try { + const asset = await this.findOne({ + where: { + account: { id }, + assetName, + }, + }); + + this.logger.log( + `자산 조회 완료: ${asset ? `assetId=${asset.assetId}` : '자산 없음'}`, + ); + return asset; + } catch (error) { + this.logger.error(`자산 조회 실패: ${error.message}`, error.stack); + throw new InternalServerErrorException( + '자산 조회 중 오류가 발생했습니다.', + ); + } + } + async getAssets(accountId: number): Promise { try { const assets = await this.find({ diff --git a/packages/server/src/trade-history/trade-history.repository.ts b/packages/server/src/trade-history/trade-history.repository.ts index 8af4c336..d5b3d925 100644 --- a/packages/server/src/trade-history/trade-history.repository.ts +++ b/packages/server/src/trade-history/trade-history.repository.ts @@ -12,7 +12,7 @@ export class TradeHistoryRepository extends Repository { super(TradeHistory, dataSource.createEntityManager()); } - async createTradeHistory( + async createTradeHistoryWithQR( user: User, tradeData: CreateTradeHistoryDto, queryRunner: QueryRunner, @@ -33,4 +33,27 @@ export class TradeHistoryRepository extends Repository { throw error; } } + + async createTradeHistory( + user: User, + tradeData: CreateTradeHistoryDto, + tradeTime: Date, + ): Promise { + this.logger.log(`거래 내역 생성 시작: userId=${user.id}`); + + try { + const tradeHistory = new TradeHistory(); + Object.assign(tradeHistory, { + ...tradeData, + tradeDate: tradeTime, + user, + }); + + await this.save(tradeHistory); + this.logger.log(`거래 내역 생성 완료: userId=${user.id}`); + } catch (error) { + this.logger.error(`거래 내역 생성 실패: ${error.message}`, error.stack); + throw error; + } + } } diff --git a/packages/server/src/trade/trade-ask-bid.service.ts b/packages/server/src/trade/trade-ask-bid.service.ts index 1bd44a77..11af5d0d 100644 --- a/packages/server/src/trade/trade-ask-bid.service.ts +++ b/packages/server/src/trade/trade-ask-bid.service.ts @@ -13,7 +13,7 @@ import { TRADE_TYPES } from './constants/trade.constants'; @Injectable() export class TradeAskBidService { - private readonly logger = new Logger(TradeAskBidService.name); + protected readonly logger = new Logger(TradeAskBidService.name); constructor( protected readonly dataSource: DataSource, protected readonly accountRepository: AccountRepository, @@ -31,14 +31,14 @@ export class TradeAskBidService { try { const coinLatestInfo = this.coinDataUpdaterService.getCoinLatestInfo(); if (coinLatestInfo.size === 0) return; - + const coinPrices = this.buildCoinPrices(coinLatestInfo); - + const availableTrades = await this.redisRepository.findMatchingTrades( tradeType, coinPrices, ); - + // 병렬 처리로 모든 거래를 처리 await Promise.all( availableTrades.map(async (trade) => { @@ -65,7 +65,7 @@ export class TradeAskBidService { this.logger.log(`${tradeType} 미체결 거래 처리 완료`); } } - + private buildCoinPrices(coinLatestInfo: Map): CoinPriceDto[] { const prices: CoinPriceDto[] = []; coinLatestInfo.forEach((value, key) => { diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index 5773caf4..70e5829c 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -132,7 +132,7 @@ export class AskService extends TradeAskBidService implements OnModuleInit { account: any, queryRunner: QueryRunner, ) { - const userAsset = await this.assetRepository.getAsset( + const userAsset = await this.assetRepository.getAssetWithQR( account.id, askDto.typeGiven, queryRunner, @@ -204,12 +204,12 @@ export class AskService extends TradeAskBidService implements OnModuleInit { return 0; } - const account = await this.accountRepository.getAccount( + const account = await this.accountRepository.getAccountWithQueryRunner( askDto.userId, queryRunner, ); - const userAsset = await this.assetRepository.getAsset( + const userAsset = await this.assetRepository.getAssetWithQR( account.id, askDto.typeGiven, queryRunner, @@ -240,7 +240,7 @@ export class AskService extends TradeAskBidService implements OnModuleInit { buyData.assetName = buyData.tradeCurrency; buyData.tradeCurrency = assetName; - await this.tradeHistoryRepository.createTradeHistory( + await this.tradeHistoryRepository.createTradeHistoryWithQR( user, buyData, queryRunner, diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts index 7722ad88..8c5b6fbb 100644 --- a/packages/server/src/trade/trade-bid.service.ts +++ b/packages/server/src/trade/trade-bid.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable, + Logger, OnApplicationBootstrap, OnModuleInit, UnprocessableEntityException, @@ -22,11 +23,13 @@ import { TradeNotFoundException } from './exceptions/trade.exceptions'; import { TradeAskBidService } from './trade-ask-bid.service'; import { isMainThread, Worker } from 'worker_threads'; import { query } from 'express'; +import { User } from '@src/auth/user.entity'; +import { Account } from '@src/account/account.entity'; @Injectable() export class BidService extends TradeAskBidService implements OnModuleInit { private isProcessing: { [key: number]: boolean } = {}; - + protected readonly logger = new Logger(BidService.name); onModuleInit() { this.startPendingTradesProcessor(); } @@ -64,6 +67,8 @@ export class BidService extends TradeAskBidService implements OnModuleInit { } try { let userTrade; + const startTime = new Date().getTime(); + console.log(`${user.userId} start execution time: ${startTime} ms`); // 실행 시간 출력 const transactionResult = await this.executeTransaction( async (queryRunner) => { if (bidDto.receivedAmount <= 0) { @@ -93,6 +98,10 @@ export class BidService extends TradeAskBidService implements OnModuleInit { }; }, ); + const endTime = new Date().getTime(); // 종료 시간 기록 + const duration = endTime - startTime; // 실행 시간 계산 + console.log(`${user.userId} Transaction execution time: ${duration} ms`); // 실행 시간 출력 + if (transactionResult.statusCode === 200) { const tradeData: TradeDataRedis = { tradeId: userTrade.tradeId, @@ -106,6 +115,7 @@ export class BidService extends TradeAskBidService implements OnModuleInit { }; await this.redisRepository.createTrade(tradeData); } + console.log(user.userId+"의 거래가 등록되었습니다."); return transactionResult; }catch(error){ console.log(error); @@ -142,25 +152,42 @@ export class BidService extends TradeAskBidService implements OnModuleInit { const orderbook = this.coinDataUpdaterService.getCoinOrderbookByBid(bidDto); + const user = await this.userRepository.getUser(bidDto.userId); + + const account = await this.accountRepository.getAccount(userId); + + bidDto.accountBalance = account[typeGiven]; + bidDto.account = account; + for (const order of orderbook) { try { if (order.ask_price > bidDto.receivedPrice) break; - const tradeResult = await this.executeTransaction( - async (queryRunner) => { - const account = await this.accountRepository.getAccount(userId, queryRunner); - - bidDto.accountBalance = account[typeGiven]; - bidDto.account = account; - - const remainingQuantity = await this.executeBidTrade( - bidDto, - order, - queryRunner, - ); - - return !isMinimumQuantity(remainingQuantity); - }, + // const tradeResult = await this.executeTransaction( + // async (queryRunner) => { + // const account = await this.accountRepository.getAccount(userId, queryRunner); + + // bidDto.accountBalance = account[typeGiven]; + // bidDto.account = account; + + // const remainingQuantity = await this.executeBidTrade( + // bidDto, + // order, + // user, + // account, + // queryRunner, + // ); + + // return !isMinimumQuantity(remainingQuantity); + // }, + // ); + + const remainingQuantity = await this.executeBidTrade( + bidDto, + order, + user, ); + const tradeResult = !isMinimumQuantity(remainingQuantity); + if (!tradeResult) break; } catch (error) { @@ -170,6 +197,7 @@ export class BidService extends TradeAskBidService implements OnModuleInit { throw error; } } + this.logger.error(bidDto.tradeId+ " 매수 성공"); } finally { delete this.isProcessing[bidDto.tradeId]; } @@ -178,53 +206,70 @@ export class BidService extends TradeAskBidService implements OnModuleInit { private async executeBidTrade( bidDto: TradeData, order: OrderBookEntry, - queryRunner: QueryRunner, + user: User, ): Promise { - const tradeData = await this.tradeRepository.findTradeWithLock( - bidDto.tradeId, - queryRunner, - ); - if (!tradeData || isMinimumQuantity(tradeData.quantity)) { - return 0; - } - const { ask_price, ask_size } = order; - const { userId, account, typeReceived, krw } = bidDto; + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction('READ COMMITTED'); - const buyData = { ...tradeData }; - buyData.quantity = formatQuantity( - tradeData.quantity >= ask_size ? ask_size : tradeData.quantity, - ); + try { + const tradeData = await this.tradeRepository.findTradeWithLock( + bidDto.tradeId, + queryRunner, + ); + if (!tradeData || isMinimumQuantity(tradeData.quantity)) { + return 0; + } + const { ask_price, ask_size } = order; + const { account, krw } = bidDto; - if (isMinimumQuantity(buyData.quantity)) { - return 0; - } + const buyData = { ...tradeData }; + buyData.quantity = formatQuantity( + tradeData.quantity >= ask_size ? ask_size : tradeData.quantity, + ); - buyData.price = formatQuantity(ask_price * krw); - const user = await this.userRepository.getUserByQueryRunner(userId,queryRunner); + if (isMinimumQuantity(buyData.quantity)) { + return 0; + } - await this.tradeHistoryRepository.createTradeHistory( - user, - buyData, - queryRunner, - ); + buyData.price = formatQuantity(ask_price * krw); - const asset = await this.assetRepository.getAsset( - account.id, - typeReceived, - queryRunner, - ); + const result = await this.updateTradeData(tradeData, buyData, queryRunner); + await queryRunner.commitTransaction(); + const tradeTime = new Date(); + this.tradeHistoryRepository.createTradeHistory( + user, + buyData, + tradeTime + ); + + bidDto.account.availableKRW += formatQuantity( + (bidDto.receivedPrice - buyData.price) * buyData.quantity, + ); + + bidDto.account.KRW -= formatQuantity(buyData.price * buyData.quantity); - await this.processAssetUpdate(bidDto, asset, buyData, queryRunner); - await this.updateAccountBalances(bidDto, buyData, queryRunner); - return await this.updateTradeData(tradeData, buyData, queryRunner); + this.processAssetUpdate(bidDto, account.id, buyData); + this.updateAccountBalances(bidDto, buyData, account); + + return result; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } private async processAssetUpdate( bidDto: TradeData, - asset: any, + accountId: any, buyData: any, - queryRunner: QueryRunner, ): Promise { + const asset = await this.assetRepository.getAsset( + accountId, + bidDto.typeReceived, + ); if (asset) { asset.price = formatQuantity( asset.price + buyData.price * buyData.quantity, @@ -234,14 +279,13 @@ export class BidService extends TradeAskBidService implements OnModuleInit { asset.availableQuantity + buyData.quantity, ); - await this.assetRepository.updateAssetQuantityPrice(asset, queryRunner); + await this.assetRepository.updateAssetQuantityPrice(asset); } else { await this.assetRepository.createAsset( bidDto.typeReceived, bidDto.account, formatQuantity(buyData.price * buyData.quantity), formatQuantity(buyData.quantity), - queryRunner, ); } } @@ -249,22 +293,9 @@ export class BidService extends TradeAskBidService implements OnModuleInit { private async updateAccountBalances( bidDto: TradeData, buyData: any, - queryRunner: QueryRunner, + userAccount: Account, ): Promise { - const { account, typeGiven, typeReceived } = bidDto; - const userAccount = await this.accountRepository.getAccount( - bidDto.userId, - queryRunner, - ); - - if (typeReceived === 'BTC') { - const btcQuantity = formatQuantity(account.BTC + buyData.quantity); - await this.accountRepository.updateAccountBTC( - userAccount.id, - btcQuantity, - queryRunner, - ); - } + const { typeGiven } = bidDto; const returnChange = formatQuantity(buyData.price * buyData.quantity); @@ -272,18 +303,25 @@ export class BidService extends TradeAskBidService implements OnModuleInit { (bidDto.receivedPrice - buyData.price) * buyData.quantity, ); - await this.accountRepository.updateAccountCurrency( + // await this.accountRepository.updateAccountCurrency( + // typeGiven, + // -returnChange, + // userAccount.id, + // queryRunner, + // ); + + // await this.accountRepository.updateAccountCurrency( + // 'availableKRW', + // change, + // userAccount.id, + // queryRunner, + // ); + + await this.accountRepository.updateAccountCurrencyWithBid( typeGiven, -returnChange, - userAccount.id, - queryRunner, - ); - - await this.accountRepository.updateAccountCurrency( - 'availableKRW', change, userAccount.id, - queryRunner, - ); + ) } } diff --git a/packages/server/src/trade/trade.repository.ts b/packages/server/src/trade/trade.repository.ts index 839e069a..87ab7af1 100644 --- a/packages/server/src/trade/trade.repository.ts +++ b/packages/server/src/trade/trade.repository.ts @@ -37,7 +37,7 @@ export class TradeRepository extends Repository { try { const user = await this.userRepository.getUserByQueryRunner(userId,queryRunner); - this.userRepository.validateUser(userId,queryRunner); + //this.userRepository.validateUser(userId,queryRunner); const trade = this.createTradeEntity(tradeDto, user, tradeType); const savedTrade = await queryRunner.manager.save(Trade, trade); diff --git a/packages/server/src/trade/trade.service.ts b/packages/server/src/trade/trade.service.ts index 900f26fb..66c35d6a 100644 --- a/packages/server/src/trade/trade.service.ts +++ b/packages/server/src/trade/trade.service.ts @@ -250,7 +250,7 @@ export class TradeService { tradeData: any, queryRunner: QueryRunner, ): Promise { - await this.tradeHistoryRepository.createTradeHistory( + await this.tradeHistoryRepository.createTradeHistoryWithQR( user, tradeData, queryRunner, @@ -268,7 +268,7 @@ export class TradeService { return; } - await this.assetRepository.updateAssetQuantityPrice( + await this.assetRepository.updateAssetQuantityPriceWithQR( { ...asset, quantity, price }, queryRunner, );