From 1e762d04e4eed8fd062c4fa7901a078204642bb6 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Thu, 21 Nov 2024 20:54:54 +0900 Subject: [PATCH 01/59] fix: ask error fix --- packages/server/src/trade/trade-ask.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index eaa420a2..3edffeb9 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -74,7 +74,7 @@ export class AskService implements OnModuleInit { }) }else{ userAsset.quantity = assetBalance - userAsset.price -= Math.floor(askDto.receivedPrice + askDto.receivedAmount) + userAsset.price -= askDto.receivedPrice.toFixed(8) * askDto.receivedAmount.toFixed(8) this.assetRepository.updateAssetPrice(userAsset, queryRunner); } await this.tradeRepository.createTrade(askDto, user.userId,'sell', queryRunner); From 5c1f3e23585915d75d984b73a5045d79a606d4e7 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Thu, 21 Nov 2024 22:30:08 +0900 Subject: [PATCH 02/59] =?UTF-8?q?feat:=20candle=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A7=91=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/src/trade/trade-ask.service.ts | 2 +- packages/server/src/upbit/chart.repository.ts | 6 ++ packages/server/src/upbit/chart.service.ts | 74 ++++++++++++++++++- .../upbit/coin-ticker-websocket.service.ts | 3 + 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index 3edffeb9..f4fea206 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -191,7 +191,7 @@ export class AskService implements OnModuleInit { queryRunner, ); - if (!asset && tradeData.price > buyData.price) { + if (asset && tradeData.price > buyData.price) { asset.price = Math.floor(asset.price + (tradeData.price - buyData.price) * buyData.quantity); await this.assetRepository.updateAssetPrice(asset, queryRunner); diff --git a/packages/server/src/upbit/chart.repository.ts b/packages/server/src/upbit/chart.repository.ts index 8f2bee54..f9ef2720 100644 --- a/packages/server/src/upbit/chart.repository.ts +++ b/packages/server/src/upbit/chart.repository.ts @@ -30,4 +30,10 @@ export class ChartRepository { console.error("DB Searching Error : "+error) } } + async getSimpleChartData(key){ + const data = await this.chartRedis.get(key); + if(!data){ + return false; + }else return JSON.parse(data) + } } diff --git a/packages/server/src/upbit/chart.service.ts b/packages/server/src/upbit/chart.service.ts index ce27db6c..c7bc84f0 100644 --- a/packages/server/src/upbit/chart.service.ts +++ b/packages/server/src/upbit/chart.service.ts @@ -18,10 +18,6 @@ export class ChartService implements OnModuleInit{ this.cleanQueue() } async upbitApiDoor(type,coin,to, minute){ - console.log("type : "+type) - console.log("market : "+coin) - console.log("minute : "+minute) - console.log("to : "+to) const validMinutes = ["1", "3", "5", "10", "15", "30", "60", "240"]; if (type === 'minutes') { if (!minute || !validMinutes.includes(minute)) { @@ -201,4 +197,74 @@ export class ChartService implements OnModuleInit{ } setTimeout(()=>this.cleanQueue(),100) } + makeCandle(coinData){ + const name = coinData.code; + // date와 time을 각각 파싱 + const year = coinData.trade_date.slice(0, 4); + const month = coinData.trade_date.slice(4, 6); + const day = coinData.trade_date.slice(6, 8); + + const hour = coinData.trade_time.slice(0, 2); + const minute = coinData.trade_time.slice(2, 4); + const second = coinData.trade_time.slice(4, 6); + + const tradeDate = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}`); + const kstDate = new Date(tradeDate.getTime() + 9 * 60 * 60 * 1000 * 2); + + const price = coinData.trade_price; + const timestamp = coinData.trade_timestamp; + const candle_acc_trade_volume = coinData.trade_volume + const candle_acc_trade_price = price * candle_acc_trade_volume; + const candle = { + market : name, + candle_date_time_kst : kstDate.toISOString().slice(0,19), + opening_price : price, + high_price : price, + low_price : price, + trade_price : price, + timestamp : timestamp, + candle_acc_trade_price : candle_acc_trade_price, + candle_acc_trade_volume : candle_acc_trade_volume + } + const type = ['years','months','weeks','days','minutes','seconds']; + const minute_type = ["1", "3", "5", "10", "15", "30", "60", "240"]; + type.forEach(async (key)=>{ + if(key === 'minutes'){ + const keys = []; + minute_type.forEach((min)=>{ + keys.push(this.formatDate(kstDate, key, name, min)); + }) + keys.forEach(async (min)=>{ + const candleData = await this.chartRepository.getSimpleChartData(min); + if(!candleData){ + this.chartRepository.setChartData(min,JSON.stringify(candle)) + }else{ + candleData.trade_price = price; + candleData.high_price = candleData.high_price < price ? price : candleData.high_price; + candleData.low_price = candleData.low_price > price ? price : candleData.low_price; + candleData.timestamp = timestamp; + candleData.candle_acc_trade_price = candle_acc_trade_price; + candleData.candle_acc_trade_volume += candle_acc_trade_volume; + + this.chartRepository.setChartData(min,JSON.stringify(candleData)) + } + }) + }else{ + const redisKey = this.formatDate(kstDate, key, name, null); + const candleData = await this.chartRepository.getSimpleChartData(redisKey); + if(!candleData){ + this.chartRepository.setChartData(redisKey,JSON.stringify(candle)) + }else{ + candleData.trade_price = price; + candleData.high_price = candleData.high_price < price ? price : candleData.high_price; + candleData.low_price = candleData.low_price > price ? price : candleData.low_price; + candleData.timestamp = timestamp; + candleData.candle_acc_trade_price = candle_acc_trade_price; + candleData.candle_acc_trade_volume += candle_acc_trade_volume; + + this.chartRepository.setChartData(redisKey,JSON.stringify(candleData)) + } + } + }) + } } \ No newline at end of file diff --git a/packages/server/src/upbit/coin-ticker-websocket.service.ts b/packages/server/src/upbit/coin-ticker-websocket.service.ts index 8af7110a..25cdce3c 100644 --- a/packages/server/src/upbit/coin-ticker-websocket.service.ts +++ b/packages/server/src/upbit/coin-ticker-websocket.service.ts @@ -7,6 +7,7 @@ import { UPBIT_WEBSOCKET_CONNECTION_TIME, UPBIT_WEBSOCKET_URL, } from 'common/upbit'; +import { ChartService } from './chart.service'; @Injectable() export class CoinTickerService implements OnModuleInit { @@ -16,6 +17,7 @@ export class CoinTickerService implements OnModuleInit { constructor( private readonly coinListService: CoinListService, private readonly sseService: SseService, + private readonly chartService: ChartService ) {} onModuleInit() { @@ -38,6 +40,7 @@ export class CoinTickerService implements OnModuleInit { const message = JSON.parse(data.toString()); if (message.error) throw new Error(JSON.stringify(message)); this.sseService.coinTickerSendEvent(message); + this.chartService.makeCandle(message); } catch (error) { console.error('CoinTickerWebSocket 오류:', error); } From ebad1c57868e6d76adb36921beb2f09953bd4a49 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Thu, 21 Nov 2024 22:45:47 +0900 Subject: [PATCH 03/59] =?UTF-8?q?feat:=20candle=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A7=91=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/upbit/chart.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/server/src/upbit/chart.service.ts b/packages/server/src/upbit/chart.service.ts index c7bc84f0..0944cedf 100644 --- a/packages/server/src/upbit/chart.service.ts +++ b/packages/server/src/upbit/chart.service.ts @@ -217,6 +217,7 @@ export class ChartService implements OnModuleInit{ const candle_acc_trade_price = price * candle_acc_trade_volume; const candle = { market : name, + candle_date_time_utc : kstDate.toISOString().slice(0,19), candle_date_time_kst : kstDate.toISOString().slice(0,19), opening_price : price, high_price : price, @@ -224,8 +225,12 @@ export class ChartService implements OnModuleInit{ trade_price : price, timestamp : timestamp, candle_acc_trade_price : candle_acc_trade_price, - candle_acc_trade_volume : candle_acc_trade_volume + candle_acc_trade_volume : candle_acc_trade_volume, + prev_closing_price : 0, + change_price : 0, + change_rate : 0, } + const type = ['years','months','weeks','days','minutes','seconds']; const minute_type = ["1", "3", "5", "10", "15", "30", "60", "240"]; type.forEach(async (key)=>{ From efeceb9db7b2f66e0da92b6c5d0e1467ec23b1b8 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Thu, 21 Nov 2024 22:51:59 +0900 Subject: [PATCH 04/59] =?UTF-8?q?fix:=20=EB=A7=A4=EB=8F=84=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/trade/trade-ask.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index f4fea206..3edffeb9 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -191,7 +191,7 @@ export class AskService implements OnModuleInit { queryRunner, ); - if (asset && tradeData.price > buyData.price) { + if (!asset && tradeData.price > buyData.price) { asset.price = Math.floor(asset.price + (tradeData.price - buyData.price) * buyData.quantity); await this.assetRepository.updateAssetPrice(asset, queryRunner); From cd7a5b4190781c30b5917c57526cc4e70949621b Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Thu, 21 Nov 2024 23:01:39 +0900 Subject: [PATCH 05/59] =?UTF-8?q?fix:=20=EB=A7=A4=EB=8F=84=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/trade/trade-ask.service.ts | 4 ++-- packages/server/src/trade/trade-bid.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index 3edffeb9..354d4b52 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -4,7 +4,7 @@ import { OnModuleInit, UnprocessableEntityException, } from '@nestjs/common'; -import { DataSource, QueryRunner } from 'typeorm'; +import { DataSource } from 'typeorm'; import { AccountRepository } from 'src/account/account.repository'; import { AssetRepository } from 'src/asset/asset.repository'; import { TradeRepository } from './trade.repository'; @@ -178,7 +178,7 @@ export class AskService implements OnModuleInit { const buyData = { ...tradeData }; buyData.quantity = tradeData.quantity >= bid_size ? bid_size.toFixed(8) : tradeData.quantity.toFixed(8) - buyData.price = (bid_price * krw).toFixed(8); + buyData.price = Math.floor(bid_price * krw); if(buyData.quantity<0.00000001){ await queryRunner.commitTransaction(); return true; diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts index f1708968..6fb42e07 100644 --- a/packages/server/src/trade/trade-bid.service.ts +++ b/packages/server/src/trade/trade-bid.service.ts @@ -164,7 +164,7 @@ export class BidService implements OnModuleInit { await queryRunner.commitTransaction(); return true; } - buyData.price = (ask_price * krw).toFixed(8); + buyData.price = Math.floor(ask_price * krw); const user = await this.userRepository.getUser(userId); From c520ddac38c6f1c4652a5d787a34cc4f475c6ecc Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Thu, 21 Nov 2024 23:10:59 +0900 Subject: [PATCH 06/59] =?UTF-8?q?fix:=20=EB=A7=A4=EC=88=98/=EB=A7=A4?= =?UTF-8?q?=EB=8F=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/trade/trade-ask.service.ts | 6 +++--- packages/server/src/trade/trade-bid.service.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index 354d4b52..89355823 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -74,7 +74,7 @@ export class AskService implements OnModuleInit { }) }else{ userAsset.quantity = assetBalance - userAsset.price -= askDto.receivedPrice.toFixed(8) * askDto.receivedAmount.toFixed(8) + userAsset.price -= parseFloat(askDto.receivedPrice.toFixed(8)) * parseFloat(askDto.receivedAmount.toFixed(8)) this.assetRepository.updateAssetPrice(userAsset, queryRunner); } await this.tradeRepository.createTrade(askDto, user.userId,'sell', queryRunner); @@ -177,8 +177,8 @@ export class AskService implements OnModuleInit { try { const buyData = { ...tradeData }; buyData.quantity = - tradeData.quantity >= bid_size ? bid_size.toFixed(8) : tradeData.quantity.toFixed(8) - buyData.price = Math.floor(bid_price * krw); + tradeData.quantity >= bid_size ? parseFloat(bid_size.toFixed(8)) : parseFloat(tradeData.quantity.toFixed(8)) + buyData.price = parseFloat((bid_price * krw).toFixed(8)); if(buyData.quantity<0.00000001){ await queryRunner.commitTransaction(); return true; diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts index 6fb42e07..c7c8a428 100644 --- a/packages/server/src/trade/trade-bid.service.ts +++ b/packages/server/src/trade/trade-bid.service.ts @@ -159,7 +159,7 @@ export class BidService implements OnModuleInit { let result = false; try { const buyData = {...tradeData}; - buyData.quantity = buyData.quantity >= ask_size ? ask_size.toFixed(8) : buyData.quantity.toFixed(8) + buyData.quantity = buyData.quantity >= ask_size ? parseFloat(ask_size.toFixed(8)) : parseFloat(buyData.quantity.toFixed(8)) if(buyData.quantity<0.00000001){ await queryRunner.commitTransaction(); return true; From 0e74f3ccb95044b3a7a34b13591b200fa96728f2 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Fri, 22 Nov 2024 11:02:54 +0900 Subject: [PATCH 07/59] =?UTF-8?q?fix=20:=20=EB=A7=A4=EB=8F=84=20=EB=A7=A4?= =?UTF-8?q?=EB=8F=84=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/trade/trade-ask.service.ts | 6 +++--- packages/server/src/trade/trade-bid.service.ts | 18 ++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index 89355823..4235229f 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -67,7 +67,7 @@ export class AskService implements OnModuleInit { }); } const userAsset = await this.checkCurrency(askDto, userAccount, queryRunner) - const assetBalance = userAsset.quantity - askDto.receivedAmount; + const assetBalance = parseFloat((userAsset.quantity - askDto.receivedAmount).toFixed(8)); if(assetBalance <= 0){ await this.assetRepository.delete({ assetId: userAsset.assetId @@ -192,7 +192,7 @@ export class AskService implements OnModuleInit { ); if (!asset && tradeData.price > buyData.price) { - asset.price = Math.floor(asset.price + (tradeData.price - buyData.price) * buyData.quantity); + asset.price = parseFloat((asset.price + (tradeData.price - buyData.price) * buyData.quantity).toFixed(8)); await this.assetRepository.updateAssetPrice(asset, queryRunner); } @@ -205,7 +205,7 @@ export class AskService implements OnModuleInit { const BTC_QUANTITY = account.BTC - buyData.quantity await this.accountRepository.updateAccountBTC(account.id, BTC_QUANTITY, queryRunner) } - const change = Math.floor(account[typeReceived] + buyData.price * buyData.quantity) + const change = parseFloat((account[typeReceived] + buyData.price * buyData.quantity).toFixed(8)) await this.accountRepository.updateAccountCurrency(typeReceived, change, account.id, queryRunner) diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts index c7c8a428..e5a3d14d 100644 --- a/packages/server/src/trade/trade-bid.service.ts +++ b/packages/server/src/trade/trade-bid.service.ts @@ -62,7 +62,7 @@ export class BidService implements OnModuleInit { const accountBalance = await this.checkCurrency(user, bidDto); await this.accountRepository.updateAccountCurrency( bidDto.typeGiven, - Math.floor(accountBalance), + parseFloat(accountBalance.toFixed(8)), userAccount.id, queryRunner, ); @@ -87,7 +87,7 @@ export class BidService implements OnModuleInit { } async checkCurrency(user, bidDto) { const { typeGiven, receivedPrice, receivedAmount } = bidDto; - const givenAmount = Math.floor(receivedPrice * receivedAmount); + const givenAmount = parseFloat((receivedPrice * receivedAmount).toFixed(8)); const userAccount = await this.accountRepository.findOne({ where: { user: { id: user.userId }, @@ -164,8 +164,7 @@ export class BidService implements OnModuleInit { await queryRunner.commitTransaction(); return true; } - buyData.price = Math.floor(ask_price * krw); - + buyData.price = parseFloat((ask_price * krw).toFixed(8)); const user = await this.userRepository.getUser(userId); await this.tradeHistoryRepository.createTradeHistory( @@ -179,27 +178,26 @@ export class BidService implements OnModuleInit { }); if (asset) { - asset.price = Math.floor(asset.price + buyData.price * buyData.quantity); - asset.quantity += buyData.quantity; - + asset.price = parseFloat((asset.price + buyData.price * buyData.quantity).toFixed(8)); + asset.quantity += parseFloat(buyData.quantity.toFixed(8)); await this.assetRepository.updateAssetQuantityPrice(asset, queryRunner); } else { await this.assetRepository.createAsset( bidDto, - Math.floor(buyData.price * buyData.quantity), + parseFloat((buyData.price * buyData.quantity).toFixed(8)), buyData.quantity, queryRunner, ); } - tradeData.quantity -= buyData.quantity; + tradeData.quantity -= parseFloat(buyData.quantity.toFixed(8)); if (tradeData.quantity === 0) { await this.tradeRepository.deleteTrade(tradeId, queryRunner); } else await this.tradeRepository.updateTradeTransaction(tradeData, queryRunner); const change = (tradeData.price - buyData.price) * buyData.quantity; - const returnChange = Math.floor(change + account[typeGiven]) + const returnChange = parseFloat((change + account[typeGiven]).toFixed(8)) const new_asset = await this.assetRepository.findOne({ where: {account:{id:account.id}, assetName: "BTC"} }) From da7ce38cab04b584839ed14e6e2d8c9ff1de25ac Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Fri, 22 Nov 2024 13:59:02 +0900 Subject: [PATCH 08/59] =?UTF-8?q?fix:=20=ED=8D=BC=EC=84=BC=ED=8A=B8=20api?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/trade/trade-ask.service.ts | 2 +- packages/server/src/trade/trade-bid.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index 4235229f..e3a50040 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -44,7 +44,7 @@ export class AskService implements OnModuleInit { } }) if(!asset) return 0; - return asset.quantity * (percent / 100); + return parseFloat((asset.quantity * (percent / 100)).toFixed(8)); } async createAskTrade(user, askDto) { if(askDto.receivedAmount * askDto.receivedPrice < 5000) throw new BadRequestException(); diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts index e5a3d14d..fc2cad2d 100644 --- a/packages/server/src/trade/trade-bid.service.ts +++ b/packages/server/src/trade/trade-bid.service.ts @@ -37,7 +37,7 @@ export class BidService implements OnModuleInit { async calculatePercentBuy(user, moneyType: string, percent: number) { const money = await this.accountRepository.getMyMoney(user, moneyType); - return Number(money) * (percent / 100); + return parseFloat((money * (percent / 100)).toFixed(8)); } async createBidTrade(user, bidDto) { if(bidDto.receivedAmount * bidDto.receivedPrice < 5000) throw new BadRequestException(); From af001bbddb94ba96026acc642ef1f88d922bb2e2 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Fri, 22 Nov 2024 14:05:39 +0900 Subject: [PATCH 09/59] =?UTF-8?q?fix:=20=ED=8D=BC=EC=84=BC=ED=8A=B8=20api?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/trade/trade-ask.service.ts | 2 +- packages/server/src/trade/trade-bid.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index e3a50040..a1513611 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -211,7 +211,7 @@ export class AskService implements OnModuleInit { tradeData.quantity -= buyData.quantity; - if (tradeData.quantity === 0) { + if (tradeData.quantity <= 0.00000001) { await this.tradeRepository.deleteTrade(tradeId, queryRunner); } else{ await this.tradeRepository.updateTradeTransaction( diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts index fc2cad2d..e4024385 100644 --- a/packages/server/src/trade/trade-bid.service.ts +++ b/packages/server/src/trade/trade-bid.service.ts @@ -192,7 +192,7 @@ export class BidService implements OnModuleInit { tradeData.quantity -= parseFloat(buyData.quantity.toFixed(8)); - if (tradeData.quantity === 0) { + if (tradeData.quantity <= 0.00000001) { await this.tradeRepository.deleteTrade(tradeId, queryRunner); } else await this.tradeRepository.updateTradeTransaction(tradeData, queryRunner); From f4d59b481d9c04e37dd3ee9a24411a0da7757f09 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Fri, 22 Nov 2024 20:05:54 +0900 Subject: [PATCH 10/59] =?UTF-8?q?fix:=20trade,=20trade-history=20user-casc?= =?UTF-8?q?ade=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/trade-history/trade-history.entity.ts | 50 ++++++++++--------- packages/server/src/trade/trade.entity.ts | 4 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/server/src/trade-history/trade-history.entity.ts b/packages/server/src/trade-history/trade-history.entity.ts index 2b1f0dbe..de11a547 100644 --- a/packages/server/src/trade-history/trade-history.entity.ts +++ b/packages/server/src/trade-history/trade-history.entity.ts @@ -1,39 +1,41 @@ import { User } from '@src/auth/user.entity'; import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - CreateDateColumn, - UpdateDateColumn, + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, } from 'typeorm'; @Entity() export class TradeHistory { - @PrimaryGeneratedColumn() - tradeHistoryId: number; + @PrimaryGeneratedColumn() + tradeHistoryId: number; - @Column() - assetName: string; + @Column() + assetName: string; - @Column() - tradeType: string; + @Column() + tradeType: string; - @Column() - tradeCurrency: string; + @Column() + tradeCurrency: string; - @Column('double') - price: number; + @Column('double') + price: number; - @Column('double') - quantity: number; + @Column('double') + quantity: number; - @Column({ type: 'timestamp' }) - createdAt: Date; + @Column({ type: 'timestamp' }) + createdAt: Date; - @CreateDateColumn({ type: 'timestamp' }) - tradeDate: Date; + @CreateDateColumn({ type: 'timestamp' }) + tradeDate: Date; - @ManyToOne(() => User, (user) => user.tradeHistories) - user: User; + @ManyToOne(() => User, (user) => user.tradeHistories, { + onDelete: 'CASCADE', + }) + user: User; } diff --git a/packages/server/src/trade/trade.entity.ts b/packages/server/src/trade/trade.entity.ts index 8d0fb4aa..f238d427 100644 --- a/packages/server/src/trade/trade.entity.ts +++ b/packages/server/src/trade/trade.entity.ts @@ -30,6 +30,8 @@ export class Trade { @CreateDateColumn({ type: 'timestamp' }) createdAt: Date; - @ManyToOne(() => User, (user) => user.trades) + @ManyToOne(() => User, (user) => user.trades, { + onDelete: 'CASCADE', + }) user: User; } From f4d9fdb93783b61d652ffeafd5665a984875e1b3 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Fri, 22 Nov 2024 20:07:03 +0900 Subject: [PATCH 11/59] =?UTF-8?q?chore:=20GUEST=5FID=5FTTL=20const?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/auth/auth.service.ts | 3 ++- packages/server/src/auth/constants.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index d3375bc9..521dd691 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -5,6 +5,7 @@ import { DEFAULT_BTC, DEFAULT_KRW, DEFAULT_USDT, + GUEST_ID_TTL, jwtConstants, } from './constants'; import { v4 as uuidv4 } from 'uuid'; @@ -44,7 +45,7 @@ export class AuthService { await this.redisRepository.setAuthData( `guest:${guestUser.id}`, JSON.stringify({ userId: guestUser.id }), - 6000, + GUEST_ID_TTL, ); const payload = { userId: guestUser.id, userName: guestUser.username }; diff --git a/packages/server/src/auth/constants.ts b/packages/server/src/auth/constants.ts index 06bd4fce..b7dba053 100644 --- a/packages/server/src/auth/constants.ts +++ b/packages/server/src/auth/constants.ts @@ -6,3 +6,4 @@ export const jwtConstants = { export const DEFAULT_KRW = 30000000; export const DEFAULT_USDT = 300000; export const DEFAULT_BTC = 0; +export const GUEST_ID_TTL = 86400; From 355ecdffb2a0d14136b618bd0d7b9a29f4f8d831 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Fri, 22 Nov 2024 20:49:39 +0900 Subject: [PATCH 12/59] =?UTF-8?q?feat:=20auth=20refresh=20token=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/auth/auth.controller.ts | 112 ++++++++++--------- packages/server/src/auth/auth.service.ts | 115 +++++++++++++++++--- 2 files changed, 158 insertions(+), 69 deletions(-) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index b2214dcd..3c2633e0 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -1,69 +1,77 @@ import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Post, - Request, - UseGuards, + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Post, + Request, + UseGuards, } from '@nestjs/common'; import { AuthGuard } from './auth.guard'; import { AuthService } from './auth.service'; import { - ApiBody, - ApiBearerAuth, - ApiSecurity, - ApiResponse, + ApiBody, + ApiBearerAuth, + ApiSecurity, + ApiResponse, } from '@nestjs/swagger'; import { SignInDto } from './dtos/sign-in.dto'; import { SignUpDto } from './dtos/sign-up.dto'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} + constructor(private authService: AuthService) {} - @ApiBody({ type: SignInDto }) - @HttpCode(HttpStatus.OK) - @Post('login') - signIn(@Body() signInDto: Record) { - return this.authService.signIn(signInDto.username); - } + @ApiBody({ type: SignInDto }) + @HttpCode(HttpStatus.OK) + @Post('login') + signIn(@Body() signInDto: Record) { + return this.authService.signIn(signInDto.username); + } - @HttpCode(HttpStatus.OK) - @Post('guest-login') - guestSignIn() { - return this.authService.guestSignIn(); - } + @HttpCode(HttpStatus.OK) + @Post('guest-login') + guestSignIn() { + return this.authService.guestSignIn(); + } - @ApiResponse({ - status: HttpStatus.OK, - description: 'New user successfully registered', - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Invalid input or user already exists', - }) - @HttpCode(HttpStatus.CREATED) - @Post('signup') - async signUp(@Body() signUpDto: SignUpDto) { - return this.authService.signUp(signUpDto.username); - } + @ApiResponse({ + status: HttpStatus.OK, + description: 'New user successfully registered', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input or user already exists', + }) + @HttpCode(HttpStatus.CREATED) + @Post('signup') + async signUp(@Body() signUpDto: SignUpDto) { + return this.authService.signUp(signUpDto.username); + } - @ApiBearerAuth('access-token') - @ApiSecurity('access-token') - @UseGuards(AuthGuard) - @Delete('logout') - logout(@Request() req) { - return this.authService.logout(req.user.userId); - } + @ApiBearerAuth('access-token') + @ApiSecurity('access-token') + @UseGuards(AuthGuard) + @Delete('logout') + logout(@Request() req) { + return this.authService.logout(req.user.userId); + } - @UseGuards(AuthGuard) - @ApiBearerAuth('access-token') - @ApiSecurity('access-token') - @Get('profile') - getProfile(@Request() req) { - return req.user; - } + @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') + @ApiSecurity('access-token') + @Get('profile') + getProfile(@Request() req) { + return req.user; + } + + @ApiBearerAuth('refresh-token') + @ApiSecurity('refresh-token') + @HttpCode(HttpStatus.OK) + @Post('refresh') + refreshTokens(@Body() body: { userId: number; refreshToken: string }) { + return this.authService.refreshTokens(body.userId, body.refreshToken); + } } diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index 521dd691..16f13289 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -1,4 +1,9 @@ -import { ConflictException, Injectable } from '@nestjs/common'; +import { + ConflictException, + ForbiddenException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { UserRepository } from './user.repository'; import { JwtService } from '@nestjs/jwt'; import { @@ -23,19 +28,35 @@ export class AuthService { this.createAdminUser(); } - async signIn(username: string): Promise<{ access_token: string }> { + async signIn( + username: string, + ): Promise<{ access_token: string; refresh_token: string }> { const user = await this.userRepository.findOneBy({ username }); + if (!user) { + throw new UnauthorizedException('Invalid credentials'); + } + const payload = { userId: user.id, userName: user.username }; + const accessToken = await this.jwtService.signAsync(payload, { + secret: jwtConstants.secret, + expiresIn: '1d', + }); + const refreshToken = uuidv4(); + + await this.redisRepository.setAuthData( + `refresh:${user.id}`, + refreshToken, + GUEST_ID_TTL, + ); + return { - access_token: await this.jwtService.signAsync(payload, { - secret: jwtConstants.secret, - expiresIn: '1d', - }), + access_token: accessToken, + refresh_token: refreshToken, }; } - async guestSignIn(): Promise<{ access_token: string }> { - try{ + async guestSignIn(): Promise<{ access_token: string; refresh_token: string }> { + try { const username = `guest_${uuidv4()}`; await this.signUp(username, true); @@ -49,17 +70,75 @@ export class AuthService { ); const payload = { userId: guestUser.id, userName: guestUser.username }; + const accessToken = await this.jwtService.signAsync(payload, { + secret: jwtConstants.secret, + expiresIn: '1d', + }); + + const refreshToken = uuidv4(); + + await this.redisRepository.setAuthData( + `refresh:${guestUser.id}`, + refreshToken, + GUEST_ID_TTL, + ); + return { - access_token: await this.jwtService.signAsync(payload, { - secret: jwtConstants.secret, - expiresIn: '1d', - }), + access_token: accessToken, + refresh_token: refreshToken, }; - }catch(error){ - console.error(error) + + } catch (error) { + console.error(error); } } + async refreshTokens( + userId: number, + refreshToken: string, + ): Promise<{ access_token: string; refresh_token: string }> { + const storedToken = await this.redisRepository.getAuthData( + `refresh:${userId}`, + ); + + if (!storedToken) { + throw new ForbiddenException({ + message: 'Refresh token has expired', + errorCode: 'REFRESH_TOKEN_EXPIRED', + }); + } + + if (storedToken !== refreshToken) { + throw new UnauthorizedException({ + message: 'Invalid refresh token', + errorCode: 'INVALID_REFRESH_TOKEN', + }); + } + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new UnauthorizedException('User not found'); + } + + const payload = { userId: user.id, userName: user.username }; + const newAccessToken = await this.jwtService.signAsync(payload, { + secret: jwtConstants.secret, + expiresIn: '1d', + }); + const newRefreshToken = uuidv4(); + + await this.redisRepository.setAuthData( + `refresh:${user.id}`, + newRefreshToken, + GUEST_ID_TTL, + ); + + return { + access_token: newAccessToken, + refresh_token: newRefreshToken, + }; + } + async signUp( username: string, isGuest = false, @@ -89,19 +168,21 @@ export class AuthService { } async logout(userId: number): Promise<{ message: string }> { - try{ + try { const user = await this.userRepository.findOneBy({ id: userId }); if (!user) { throw new Error('User not found'); } + await this.redisRepository.deleteAuthData(`refresh:${userId}`); + if (user.isGuest) { await this.userRepository.delete({ id: userId }); return { message: 'Guest user data successfully deleted' }; } - }catch(error){ - console.error(error) + } catch (error) { + console.error(error); } } From 27acfb14b769f36282a2ae43ed3d50126db3a27c Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Fri, 22 Nov 2024 21:34:55 +0900 Subject: [PATCH 13/59] =?UTF-8?q?feat:=20refresh=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B0=8F=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/auth/auth.controller.ts | 18 ++- packages/server/src/auth/auth.service.ts | 140 +++++++++----------- packages/server/src/auth/constants.ts | 10 +- packages/server/src/main.ts | 67 +++++----- 4 files changed, 118 insertions(+), 117 deletions(-) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index 3c2633e0..64f89b20 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -67,11 +67,21 @@ export class AuthController { return req.user; } - @ApiBearerAuth('refresh-token') - @ApiSecurity('refresh-token') + @ApiBody({ + schema: { + type: 'object', + properties: { + refreshToken: { + type: 'string', + description: 'Refresh token used for renewing access token', + example: 'your-refresh-token', + }, + }, + }, + }) @HttpCode(HttpStatus.OK) @Post('refresh') - refreshTokens(@Body() body: { userId: number; refreshToken: string }) { - return this.authService.refreshTokens(body.userId, body.refreshToken); + refreshTokens(@Body() body: { refreshToken: string }) { + return this.authService.refreshTokens(body.refreshToken); } } diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index 16f13289..9a2d4e67 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -7,10 +7,12 @@ import { import { UserRepository } from './user.repository'; import { JwtService } from '@nestjs/jwt'; import { + ACCESS_TOKEN_TTL, DEFAULT_BTC, DEFAULT_KRW, DEFAULT_USDT, GUEST_ID_TTL, + REFRESH_TOKEN_TTL, jwtConstants, } from './constants'; import { v4 as uuidv4 } from 'uuid'; @@ -35,18 +37,46 @@ export class AuthService { if (!user) { throw new UnauthorizedException('Invalid credentials'); } + return this.generateTokens(user.id, user.username); + } + + async guestSignIn(): Promise<{ + access_token: string; + refresh_token: string; + }> { + const username = `guest_${uuidv4()}`; + await this.signUp(username, true); + + const guestUser = await this.userRepository.findOneBy({ username }); + if (!guestUser) { + throw new UnauthorizedException('Guest user creation failed'); + } + return this.generateTokens(guestUser.id, guestUser.username); + } + + private async generateTokens( + userId: number, + username: string, + ): Promise<{ access_token: string; refresh_token: string }> { + const payload = { userId, userName: username }; - const payload = { userId: user.id, userName: user.username }; const accessToken = await this.jwtService.signAsync(payload, { secret: jwtConstants.secret, - expiresIn: '1d', + expiresIn: ACCESS_TOKEN_TTL, }); - const refreshToken = uuidv4(); + + const refreshToken = await this.jwtService.signAsync( + { userId }, + { + secret: jwtConstants.refreshSecret, + expiresIn: REFRESH_TOKEN_TTL, + }, + ); await this.redisRepository.setAuthData( - `refresh:${user.id}`, + `refresh:${userId}`, refreshToken, - GUEST_ID_TTL, + REFRESH_TOKEN_TTL, ); return { @@ -55,88 +85,44 @@ export class AuthService { }; } - async guestSignIn(): Promise<{ access_token: string; refresh_token: string }> { + async refreshTokens( + refreshToken: string, + ): Promise<{ access_token: string; refresh_token: string }> { try { - const username = `guest_${uuidv4()}`; - - await this.signUp(username, true); - - const guestUser = await this.userRepository.findOneBy({ username }); - - await this.redisRepository.setAuthData( - `guest:${guestUser.id}`, - JSON.stringify({ userId: guestUser.id }), - GUEST_ID_TTL, - ); - - const payload = { userId: guestUser.id, userName: guestUser.username }; - const accessToken = await this.jwtService.signAsync(payload, { - secret: jwtConstants.secret, - expiresIn: '1d', + const payload = await this.jwtService.verifyAsync(refreshToken, { + secret: jwtConstants.refreshSecret, }); + const userId = payload.userId; - const refreshToken = uuidv4(); - - await this.redisRepository.setAuthData( - `refresh:${guestUser.id}`, - refreshToken, - GUEST_ID_TTL, + const storedToken = await this.redisRepository.getAuthData( + `refresh:${userId}`, ); - return { - access_token: accessToken, - refresh_token: refreshToken, - }; - - } catch (error) { - console.error(error); - } - } - - async refreshTokens( - userId: number, - refreshToken: string, - ): Promise<{ access_token: string; refresh_token: string }> { - const storedToken = await this.redisRepository.getAuthData( - `refresh:${userId}`, - ); + if (!storedToken) { + throw new ForbiddenException({ + message: 'Refresh token has expired', + errorCode: 'REFRESH_TOKEN_EXPIRED', + }); + } - if (!storedToken) { - throw new ForbiddenException({ - message: 'Refresh token has expired', - errorCode: 'REFRESH_TOKEN_EXPIRED', - }); - } + if (storedToken !== refreshToken) { + throw new UnauthorizedException({ + message: 'Invalid refresh token', + errorCode: 'INVALID_REFRESH_TOKEN', + }); + } - if (storedToken !== refreshToken) { + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new UnauthorizedException('User not found'); + } + return this.generateTokens(user.id, user.username); + } catch (error) { throw new UnauthorizedException({ - message: 'Invalid refresh token', - errorCode: 'INVALID_REFRESH_TOKEN', + message: 'Failed to refresh tokens', + errorCode: 'TOKEN_REFRESH_FAILED', }); } - - const user = await this.userRepository.findOneBy({ id: userId }); - if (!user) { - throw new UnauthorizedException('User not found'); - } - - const payload = { userId: user.id, userName: user.username }; - const newAccessToken = await this.jwtService.signAsync(payload, { - secret: jwtConstants.secret, - expiresIn: '1d', - }); - const newRefreshToken = uuidv4(); - - await this.redisRepository.setAuthData( - `refresh:${user.id}`, - newRefreshToken, - GUEST_ID_TTL, - ); - - return { - access_token: newAccessToken, - refresh_token: newRefreshToken, - }; } async signUp( diff --git a/packages/server/src/auth/constants.ts b/packages/server/src/auth/constants.ts index b7dba053..4d8f03ae 100644 --- a/packages/server/src/auth/constants.ts +++ b/packages/server/src/auth/constants.ts @@ -1,9 +1,15 @@ export const jwtConstants = { secret: - 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.', + 'superSecureAccessTokenSecret', + refreshSecret: + 'superSecureAccessTokenSecret_superSecureAccessTokenSecret', }; export const DEFAULT_KRW = 30000000; export const DEFAULT_USDT = 300000; export const DEFAULT_BTC = 0; -export const GUEST_ID_TTL = 86400; + +export const GUEST_ID_TTL = 24 * 3600; + +export const REFRESH_TOKEN_TTL = 7 * 24 * 3600; +export const ACCESS_TOKEN_TTL = '1d'; \ No newline at end of file diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index 2fb2fa8d..a36b6bee 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -1,9 +1,9 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { - SwaggerModule, - DocumentBuilder, - SwaggerCustomOptions, + SwaggerModule, + DocumentBuilder, + SwaggerCustomOptions, } from '@nestjs/swagger'; import { config } from 'dotenv'; import { setupSshTunnel } from './configs/ssh-tunnel'; @@ -12,39 +12,38 @@ import { AllExceptionsFilter } from 'common/all-exceptions.filter'; config(); async function bootstrap() { - await setupSshTunnel(); - const app = await NestFactory.create(AppModule); - app.enableCors({ - origin: true, - methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', - credentials: true, - }); + await setupSshTunnel(); + const app = await NestFactory.create(AppModule); + app.enableCors({ + origin: true, + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + credentials: true, + }); - const config = new DocumentBuilder() - .setTitle('CorinEE API example') - .setDescription('CorinEE API description') - .setVersion('1.0') - .addTag('corinee') - .addBearerAuth( - { - type: 'http', - scheme: 'bearer', - name: 'Authorization', - in: 'header', - }, - 'access-token', - ) - .build(); - const customOptions: SwaggerCustomOptions = { - swaggerOptions: { - persistAuthorization: true, - }, - }; - const documentFactory = () => SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, documentFactory); + const config = new DocumentBuilder() + .setTitle('CorinEE API example') + .setDescription('CorinEE API description') + .setVersion('1.0') + .addTag('corinee') + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Access token used for authentication', + }, + 'access-token', + ).build(); + const customOptions: SwaggerCustomOptions = { + swaggerOptions: { + persistAuthorization: true, + }, + }; + const documentFactory = () => SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, documentFactory); - app.setGlobalPrefix('api'); - app.useGlobalFilters(new AllExceptionsFilter()); + app.setGlobalPrefix('api'); + app.useGlobalFilters(new AllExceptionsFilter()); await app.listen(process.env.PORT ?? 3000); } From ac2659a6ce4114e92d35a9313f5a73d813a13b21 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Sat, 23 Nov 2024 00:11:08 +0900 Subject: [PATCH 14/59] =?UTF-8?q?feat:=20google=20oauth=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/package.json | 7 ++ packages/server/src/auth/auth.controller.ts | 41 ++++++- packages/server/src/auth/auth.guard.ts | 2 - packages/server/src/auth/auth.module.ts | 35 ++++-- packages/server/src/auth/auth.service.ts | 105 ++++++++++++------ packages/server/src/auth/dtos/sign-up.dto.ts | 16 ++- .../src/auth/strategies/google.strategy.ts | 36 ++++++ .../src/auth/strategies/kakao.strategy.ts | 35 ++++++ packages/server/src/auth/user.entity.ts | 9 ++ 9 files changed, 238 insertions(+), 48 deletions(-) create mode 100644 packages/server/src/auth/strategies/google.strategy.ts create mode 100644 packages/server/src/auth/strategies/kakao.strategy.ts diff --git a/packages/server/package.json b/packages/server/package.json index 6a9d36f6..cecfd906 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -25,6 +25,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.7", "@nestjs/schedule": "^4.1.1", @@ -41,6 +42,9 @@ "ioredis": "^5.4.1", "js-yaml": "^4.1.0", "mysql2": "^3.11.3", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-kakao": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "tunnel-ssh": "^5.1.2", @@ -58,6 +62,9 @@ "@types/jest": "^29.5.2", "@types/js-yaml": "^4", "@types/node": "^20.3.1", + "@types/passport": "^0", + "@types/passport-google-oauth20": "^2", + "@types/passport-kakao": "^1", "@types/socket.io": "^3.0.2", "@types/supertest": "^6.0.0", "@types/ws": "^8.5.13", diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index 64f89b20..814981fc 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -10,6 +10,7 @@ import { UseGuards, } from '@nestjs/common'; import { AuthGuard } from './auth.guard'; +import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; import { ApiBody, @@ -37,6 +38,44 @@ export class AuthController { return this.authService.guestSignIn(); } + @Get('google') + @UseGuards(PassportAuthGuard('google')) + async googleLogin() { + } + + @Get('google/callback') + @UseGuards(PassportAuthGuard('google')) + async googleLoginCallback(@Request() req): Promise { + const googleUser = req.user; // Passport가 주입한 사용자 정보 + + const signUpDto: SignUpDto = { + name: googleUser.name, + email: googleUser.email, + provider: googleUser.provider, + providerId: googleUser.id, + isGuest: false, + }; + + // 사용자 정보를 기반으로 토큰 생성 + const tokens = await this.authService.validateOAuthLogin(signUpDto); + return { + message: 'Google login successful', + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + }; + } + + @Get('kakao') + @UseGuards(PassportAuthGuard('kakao')) + async kakaoLogin() { + } + + @Get('kakao/callback') + @UseGuards(PassportAuthGuard('kakao')) + kakaoLoginCallback(@Request() req) { + return req.user; + } + @ApiResponse({ status: HttpStatus.OK, description: 'New user successfully registered', @@ -48,7 +87,7 @@ export class AuthController { @HttpCode(HttpStatus.CREATED) @Post('signup') async signUp(@Body() signUpDto: SignUpDto) { - return this.authService.signUp(signUpDto.username); + return this.authService.signUp(signUpDto); } @ApiBearerAuth('access-token') diff --git a/packages/server/src/auth/auth.guard.ts b/packages/server/src/auth/auth.guard.ts index 025a47e0..705989d6 100644 --- a/packages/server/src/auth/auth.guard.ts +++ b/packages/server/src/auth/auth.guard.ts @@ -22,8 +22,6 @@ export class AuthGuard implements CanActivate { const payload = await this.jwtService.verifyAsync(token, { secret: jwtConstants.secret, }); - // 💡 We're assigning the payload to the request object here - // so that we can access it in our route handlers request['user'] = payload; } catch { throw new UnauthorizedException(); diff --git a/packages/server/src/auth/auth.module.ts b/packages/server/src/auth/auth.module.ts index 3dafe996..dbce4048 100644 --- a/packages/server/src/auth/auth.module.ts +++ b/packages/server/src/auth/auth.module.ts @@ -8,18 +8,29 @@ import { AuthService } from './auth.service'; import { AccountRepository } from 'src/account/account.repository'; import { AuthController } from './auth.controller'; import { AccountModule } from 'src/account/account.module'; +import { KakaoStrategy } from './strategies/kakao.strategy'; +import { GoogleStrategy } from './strategies/google.strategy'; +import { PassportModule } from '@nestjs/passport'; @Module({ - imports: [ - TypeOrmModule.forFeature([User]), - JwtModule.register({ - global: true, - secret: jwtConstants.secret, - signOptions: { expiresIn: '6000s' }, - }), - AccountModule, - ], - providers: [UserRepository, AccountRepository, AuthService, JwtService], - controllers: [AuthController], - exports: [UserRepository], + imports: [ + TypeOrmModule.forFeature([User]), + JwtModule.register({ + global: true, + secret: jwtConstants.secret, + signOptions: { expiresIn: '6000s' }, + }), + AccountModule, + PassportModule + ], + providers: [ + UserRepository, + AccountRepository, + AuthService, + JwtService, + GoogleStrategy, + KakaoStrategy, + ], + controllers: [AuthController], + exports: [UserRepository], }) export class AuthModule {} diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index 9a2d4e67..5e3f7a57 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -19,6 +19,7 @@ import { v4 as uuidv4 } from 'uuid'; import { AccountRepository } from 'src/account/account.repository'; import { RedisRepository } from 'src/redis/redis.repository'; import { User } from './user.entity'; +import { SignUpDto } from './dtos/sign-up.dto'; @Injectable() export class AuthService { constructor( @@ -44,16 +45,86 @@ export class AuthService { access_token: string; refresh_token: string; }> { - const username = `guest_${uuidv4()}`; - await this.signUp(username, true); + const guestName = `guest_${uuidv4()}`; + const user = { name: guestName, isGuest: true }; - const guestUser = await this.userRepository.findOneBy({ username }); + await this.signUp(user); + + const guestUser = await this.userRepository.findOneBy({ + username: guestName, + }); if (!guestUser) { throw new UnauthorizedException('Guest user creation failed'); } return this.generateTokens(guestUser.id, guestUser.username); } + async signUp(user: { + name: string; + email?: string; + provider?: string; + providerId?: string; + isGuest?: boolean; + }): Promise<{ message: string }> { + const { name, email, provider, providerId, isGuest } = user; + + const existingUser = isGuest + ? await this.userRepository.findOneBy({ username: name }) + : await this.userRepository.findOne({ + where: { provider, providerId }, + }); + + if (existingUser) { + throw new ConflictException('User already exists'); + } + + const newUser = await this.userRepository.save({ + username: name, + email, + provider, + providerId, + isGuest, + }); + + await this.accountRepository.save({ + user: newUser, + KRW: DEFAULT_KRW, + USDT: DEFAULT_USDT, + BTC: DEFAULT_BTC, + }); + + return { + message: isGuest + ? 'Guest user successfully registered' + : 'User successfully registered', + }; + } + + async validateOAuthLogin( + signUpDto: SignUpDto, + ): Promise<{ access_token: string; refresh_token: string }> { + const { name, email, provider, providerId, isGuest } = signUpDto; + + let user = await this.userRepository.findOne({ + where: { provider, providerId }, + }); + + if (!user) { + await this.signUp( + { name, email, provider, providerId, isGuest: false }, + ); + user = await this.userRepository.findOne({ + where: { provider, providerId }, + }); + } + + if (!user) { + throw new UnauthorizedException('OAuth user creation failed'); + } + + return this.generateTokens(user.id, user.username); + } + private async generateTokens( userId: number, username: string, @@ -125,34 +196,6 @@ export class AuthService { } } - async signUp( - username: string, - isGuest = false, - ): Promise<{ message: string }> { - const existingUser = await this.userRepository.findOneBy({ username }); - if (existingUser) { - throw new ConflictException('Username already exists'); - } - - const newUser = await this.userRepository.save({ - username, - isGuest, - }); - - await this.accountRepository.save({ - user: newUser, - KRW: DEFAULT_KRW, - USDT: DEFAULT_USDT, - BTC: DEFAULT_BTC, - }); - - return { - message: isGuest - ? 'Guest user successfully registered' - : 'User successfully registered', - }; - } - async logout(userId: number): Promise<{ message: string }> { try { const user = await this.userRepository.findOneBy({ id: userId }); diff --git a/packages/server/src/auth/dtos/sign-up.dto.ts b/packages/server/src/auth/dtos/sign-up.dto.ts index b3d9b70a..b64fd51b 100644 --- a/packages/server/src/auth/dtos/sign-up.dto.ts +++ b/packages/server/src/auth/dtos/sign-up.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { IsBoolean, IsString } from 'class-validator'; export class SignUpDto { @ApiProperty({ @@ -8,5 +8,17 @@ export class SignUpDto { required: true, }) @IsString() - username: string; + name: string; + + @IsString() + email: string; + + @IsBoolean() + isGuest: boolean; + + @IsString() + provider: string; + + @IsString() + providerId: string; } diff --git a/packages/server/src/auth/strategies/google.strategy.ts b/packages/server/src/auth/strategies/google.strategy.ts new file mode 100644 index 00000000..1f19495a --- /dev/null +++ b/packages/server/src/auth/strategies/google.strategy.ts @@ -0,0 +1,36 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { Strategy, VerifyCallback } from 'passport-google-oauth20'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { + constructor() { + super({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: + process.env.NODE_ENV === 'development' + ? 'http://localhost:3000/api/auth/google/callback' + : 'http://175.106.98.147:3000/api/auth/google/callback', + scope: ['email', 'profile'], + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback, + ): Promise { + const { id, displayName, emails } = profile; + const user = { + provider: 'google', + id, + name: displayName, + email: emails?.[0]?.value, + accessToken, + refreshToken, + }; + done(null, user); + } +} diff --git a/packages/server/src/auth/strategies/kakao.strategy.ts b/packages/server/src/auth/strategies/kakao.strategy.ts new file mode 100644 index 00000000..fc2971c3 --- /dev/null +++ b/packages/server/src/auth/strategies/kakao.strategy.ts @@ -0,0 +1,35 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { Strategy, Profile } from 'passport-kakao'; + +@Injectable() +export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') { + constructor() { + super({ + clientID: process.env.KAKAO_CLIENT_ID, + clientSecret: process.env.KAKAO_CLIENT_SECRET, + callbackURL: process.env.NODE_ENV === 'development' + ? 'http://localhost:3000/api/auth/google/callback' + : 'https://www.corinee.site/api/auth/google/callback', + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: Profile, + done: Function, + ): Promise { + const { id, username, _json } = profile; + const user = { + provider: 'kakao', + id, + name: username, + email: _json.kakao_account?.email, + photo: _json.properties?.profile_image, + accessToken, + refreshToken, + }; + done(null, user); + } +} diff --git a/packages/server/src/auth/user.entity.ts b/packages/server/src/auth/user.entity.ts index 4a8b6968..66196622 100644 --- a/packages/server/src/auth/user.entity.ts +++ b/packages/server/src/auth/user.entity.ts @@ -22,7 +22,16 @@ export class User extends BaseEntity { @Column() username: string; + + @Column({ nullable: true }) + email: string; + + @Column({ nullable: true }) + provider: string; + @Column({ nullable: true }) + providerId: string; + @OneToOne(() => Account, (account) => account.user, { cascade: true, onDelete: 'CASCADE', From a52ffdd07332772e8547e915c3a01b3fca5771fd Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Sat, 23 Nov 2024 00:27:21 +0900 Subject: [PATCH 15/59] =?UTF-8?q?fix:=20guestId=20redis=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/auth/auth.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index 5e3f7a57..11774387 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -53,9 +53,13 @@ export class AuthService { const guestUser = await this.userRepository.findOneBy({ username: guestName, }); - if (!guestUser) { - throw new UnauthorizedException('Guest user creation failed'); - } + + await this.redisRepository.setAuthData( + `guest:${guestUser.id}`, + JSON.stringify({ userId: guestUser.id }), + GUEST_ID_TTL, + ); + return this.generateTokens(guestUser.id, guestUser.username); } From c3290674c02121ac7aa646aabcaa63622dd182e8 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Sat, 23 Nov 2024 17:57:19 +0900 Subject: [PATCH 16/59] =?UTF-8?q?feat:=20kakao=20oauth=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/auth/auth.controller.ts | 22 +++++++++++++++---- .../src/auth/strategies/google.strategy.ts | 5 +---- .../src/auth/strategies/kakao.strategy.ts | 7 ++---- packages/server/src/auth/user.entity.ts | 1 - 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index 814981fc..5ba9c9b0 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -46,7 +46,7 @@ export class AuthController { @Get('google/callback') @UseGuards(PassportAuthGuard('google')) async googleLoginCallback(@Request() req): Promise { - const googleUser = req.user; // Passport가 주입한 사용자 정보 + const googleUser = req.user; const signUpDto: SignUpDto = { name: googleUser.name, @@ -56,7 +56,6 @@ export class AuthController { isGuest: false, }; - // 사용자 정보를 기반으로 토큰 생성 const tokens = await this.authService.validateOAuthLogin(signUpDto); return { message: 'Google login successful', @@ -72,8 +71,23 @@ export class AuthController { @Get('kakao/callback') @UseGuards(PassportAuthGuard('kakao')) - kakaoLoginCallback(@Request() req) { - return req.user; + async kakaoLoginCallback(@Request() req) { + const kakaoUser = req.user; + + const signUpDto: SignUpDto = { + name: kakaoUser.name, + email: kakaoUser.email, + provider: kakaoUser.provider, + providerId: kakaoUser.id, + isGuest: false, + }; + + const tokens = await this.authService.validateOAuthLogin(signUpDto); + return { + message: 'kakao login successful', + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + }; } @ApiResponse({ diff --git a/packages/server/src/auth/strategies/google.strategy.ts b/packages/server/src/auth/strategies/google.strategy.ts index 1f19495a..e2035579 100644 --- a/packages/server/src/auth/strategies/google.strategy.ts +++ b/packages/server/src/auth/strategies/google.strategy.ts @@ -8,10 +8,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { super({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: - process.env.NODE_ENV === 'development' - ? 'http://localhost:3000/api/auth/google/callback' - : 'http://175.106.98.147:3000/api/auth/google/callback', + callbackURL: `${process.env.CALLBACK_URL}/api/auth/google/callback`, scope: ['email', 'profile'], }); } diff --git a/packages/server/src/auth/strategies/kakao.strategy.ts b/packages/server/src/auth/strategies/kakao.strategy.ts index fc2971c3..c8010fae 100644 --- a/packages/server/src/auth/strategies/kakao.strategy.ts +++ b/packages/server/src/auth/strategies/kakao.strategy.ts @@ -7,10 +7,8 @@ export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') { constructor() { super({ clientID: process.env.KAKAO_CLIENT_ID, - clientSecret: process.env.KAKAO_CLIENT_SECRET, - callbackURL: process.env.NODE_ENV === 'development' - ? 'http://localhost:3000/api/auth/google/callback' - : 'https://www.corinee.site/api/auth/google/callback', + clientSecret: '', + callbackURL: `${process.env.CALLBACK_URL}/api/auth/kakao/callback` }); } @@ -26,7 +24,6 @@ export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') { id, name: username, email: _json.kakao_account?.email, - photo: _json.properties?.profile_image, accessToken, refreshToken, }; diff --git a/packages/server/src/auth/user.entity.ts b/packages/server/src/auth/user.entity.ts index 66196622..c9f56bad 100644 --- a/packages/server/src/auth/user.entity.ts +++ b/packages/server/src/auth/user.entity.ts @@ -12,7 +12,6 @@ import { Trade } from 'src/trade/trade.entity'; import { TradeHistory } from 'src/trade-history/trade-history.entity'; @Entity() -@Unique(['username']) export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; From 7e163748b7d784ab8b82b77b1ec697662c04fa54 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Sat, 23 Nov 2024 18:25:36 +0900 Subject: [PATCH 17/59] =?UTF-8?q?chore:=20oauth=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=B0=B0=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index e519f05b..88e24d85 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -6,7 +6,7 @@ on: - main - dev - dev-be - - feature-be-#51 + - feature-be-#101 jobs: build_and_deploy: runs-on: ubuntu-latest From 0ca63ae2588fb3984dc5bf969e0c1ac8fc8d26a5 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Sun, 24 Nov 2024 19:41:06 +0900 Subject: [PATCH 18/59] =?UTF-8?q?fix:=20=EC=BA=94=EB=93=A4=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A7=91=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/upbit/coin-ticker-websocket.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/upbit/coin-ticker-websocket.service.ts b/packages/server/src/upbit/coin-ticker-websocket.service.ts index 25cdce3c..de8582cd 100644 --- a/packages/server/src/upbit/coin-ticker-websocket.service.ts +++ b/packages/server/src/upbit/coin-ticker-websocket.service.ts @@ -40,7 +40,7 @@ export class CoinTickerService implements OnModuleInit { const message = JSON.parse(data.toString()); if (message.error) throw new Error(JSON.stringify(message)); this.sseService.coinTickerSendEvent(message); - this.chartService.makeCandle(message); + //this.chartService.makeCandle(message); } catch (error) { console.error('CoinTickerWebSocket 오류:', error); } From 9b25a76996f0ab3f19e6b8bd7aecaa249a9ee15b Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Mon, 25 Nov 2024 10:44:27 +0900 Subject: [PATCH 19/59] fix: cors methos fix --- packages/server/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index a36b6bee..828a6213 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -16,7 +16,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: true, - methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', credentials: true, }); From 3150e07aaeff359d0bde6e96f5dfdbb487450027 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Mon, 25 Nov 2024 11:30:14 +0900 Subject: [PATCH 20/59] fix: cors methos fix --- packages/server/src/auth/auth.controller.ts | 31 +++++++++++++-------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index 5ba9c9b0..ca83fb2b 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -7,6 +7,7 @@ import { HttpStatus, Post, Request, + Res, UseGuards, } from '@nestjs/common'; import { AuthGuard } from './auth.guard'; @@ -45,7 +46,10 @@ export class AuthController { @Get('google/callback') @UseGuards(PassportAuthGuard('google')) - async googleLoginCallback(@Request() req): Promise { + async googleLoginCallback( + @Request() req, + @Res() res, + ): Promise { const googleUser = req.user; const signUpDto: SignUpDto = { @@ -57,11 +61,11 @@ export class AuthController { }; const tokens = await this.authService.validateOAuthLogin(signUpDto); - return { - message: 'Google login successful', - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - }; + const frontendURL = 'http://localhost:5173'; + const redirectURL = new URL('/auth/callback', frontendURL); + redirectURL.searchParams.append('access_token', tokens.access_token); + redirectURL.searchParams.append('refresh_token', tokens.refresh_token); + return res.redirect(redirectURL.toString()); } @Get('kakao') @@ -71,7 +75,10 @@ export class AuthController { @Get('kakao/callback') @UseGuards(PassportAuthGuard('kakao')) - async kakaoLoginCallback(@Request() req) { + async kakaoLoginCallback( + @Request() req, + @Res() res + ){ const kakaoUser = req.user; const signUpDto: SignUpDto = { @@ -83,11 +90,11 @@ export class AuthController { }; const tokens = await this.authService.validateOAuthLogin(signUpDto); - return { - message: 'kakao login successful', - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - }; + const frontendURL = 'http://localhost:5173'; + const redirectURL = new URL('/auth/callback', frontendURL); + redirectURL.searchParams.append('access_token', tokens.access_token); + redirectURL.searchParams.append('refresh_token', tokens.refresh_token); + return res.redirect(redirectURL.toString()); } @ApiResponse({ From fd595efeca2845a1267b00e4c1d63e13da07a9f5 Mon Sep 17 00:00:00 2001 From: SeungGwa123 Date: Mon, 25 Nov 2024 11:56:54 +0900 Subject: [PATCH 21/59] fix: cors methos fix --- packages/server/src/auth/auth.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index ca83fb2b..9017f824 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -62,7 +62,7 @@ export class AuthController { const tokens = await this.authService.validateOAuthLogin(signUpDto); const frontendURL = 'http://localhost:5173'; - const redirectURL = new URL('/auth/callback', frontendURL); + const redirectURL = new URL('/auth/callback'); redirectURL.searchParams.append('access_token', tokens.access_token); redirectURL.searchParams.append('refresh_token', tokens.refresh_token); return res.redirect(redirectURL.toString()); @@ -91,7 +91,7 @@ export class AuthController { const tokens = await this.authService.validateOAuthLogin(signUpDto); const frontendURL = 'http://localhost:5173'; - const redirectURL = new URL('/auth/callback', frontendURL); + const redirectURL = new URL('/auth/callback'); redirectURL.searchParams.append('access_token', tokens.access_token); redirectURL.searchParams.append('refresh_token', tokens.refresh_token); return res.redirect(redirectURL.toString()); From 8938ffff9562f80cf15f08fba35091193d6a6f49 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Mon, 25 Nov 2024 12:50:18 +0900 Subject: [PATCH 22/59] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20oauth=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/auth/auth.controller.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index 9017f824..14fc46b1 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -61,10 +61,13 @@ export class AuthController { }; const tokens = await this.authService.validateOAuthLogin(signUpDto); - const frontendURL = 'http://localhost:5173'; - const redirectURL = new URL('/auth/callback'); + //const frontendURL = 'http://localhost:5173'; + const frontendURL = `${req.protocol}://${req.get('host')}`; + const redirectURL = new URL('/auth/callback', frontendURL); + redirectURL.searchParams.append('access_token', tokens.access_token); redirectURL.searchParams.append('refresh_token', tokens.refresh_token); + console.log(redirectURL); return res.redirect(redirectURL.toString()); } @@ -90,8 +93,9 @@ export class AuthController { }; const tokens = await this.authService.validateOAuthLogin(signUpDto); - const frontendURL = 'http://localhost:5173'; - const redirectURL = new URL('/auth/callback'); + //const frontendURL = 'http://localhost:5173'; + const frontendURL = `${req.protocol}://${req.get('host')}`; + const redirectURL = new URL('/auth/callback', frontendURL); redirectURL.searchParams.append('access_token', tokens.access_token); redirectURL.searchParams.append('refresh_token', tokens.refresh_token); return res.redirect(redirectURL.toString()); From 5340b81caa6bf0bdd02f10e06aaebab71cf5af0f Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Mon, 25 Nov 2024 13:22:29 +0900 Subject: [PATCH 23/59] =?UTF-8?q?fix:=20=EB=A1=9C=EC=BB=AC=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20oauth=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/auth/auth.controller.ts | 120 ++++++++++---------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index 14fc46b1..f9a61ced 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -39,67 +39,67 @@ export class AuthController { return this.authService.guestSignIn(); } - @Get('google') - @UseGuards(PassportAuthGuard('google')) - async googleLogin() { - } - - @Get('google/callback') - @UseGuards(PassportAuthGuard('google')) - async googleLoginCallback( - @Request() req, - @Res() res, - ): Promise { - const googleUser = req.user; - - const signUpDto: SignUpDto = { - name: googleUser.name, - email: googleUser.email, - provider: googleUser.provider, - providerId: googleUser.id, - isGuest: false, - }; - - const tokens = await this.authService.validateOAuthLogin(signUpDto); - //const frontendURL = 'http://localhost:5173'; - const frontendURL = `${req.protocol}://${req.get('host')}`; - const redirectURL = new URL('/auth/callback', frontendURL); - - redirectURL.searchParams.append('access_token', tokens.access_token); - redirectURL.searchParams.append('refresh_token', tokens.refresh_token); + @Get('google') + @UseGuards(PassportAuthGuard('google')) + async googleLogin() {} + + @Get('google/callback') + @UseGuards(PassportAuthGuard('google')) + async googleLoginCallback(@Request() req, @Res() res): Promise { + const googleUser = req.user; + + const signUpDto: SignUpDto = { + name: googleUser.name, + email: googleUser.email, + provider: googleUser.provider, + providerId: googleUser.id, + isGuest: false, + }; + + const tokens = await this.authService.validateOAuthLogin(signUpDto); + // 요청 Origin 기반으로 리다이렉트 URL 결정 + const origin = req.headers['origin']; + const frontendURL = + origin && origin.includes('localhost') + ? 'http://localhost:5173' + : 'https://www.corinee.site'; + const redirectURL = new URL('/auth/callback', frontendURL); + + redirectURL.searchParams.append('access_token', tokens.access_token); + redirectURL.searchParams.append('refresh_token', tokens.refresh_token); console.log(redirectURL); - return res.redirect(redirectURL.toString()); - } - - @Get('kakao') - @UseGuards(PassportAuthGuard('kakao')) - async kakaoLogin() { - } - - @Get('kakao/callback') - @UseGuards(PassportAuthGuard('kakao')) - async kakaoLoginCallback( - @Request() req, - @Res() res - ){ - const kakaoUser = req.user; - - const signUpDto: SignUpDto = { - name: kakaoUser.name, - email: kakaoUser.email, - provider: kakaoUser.provider, - providerId: kakaoUser.id, - isGuest: false, - }; - - const tokens = await this.authService.validateOAuthLogin(signUpDto); - //const frontendURL = 'http://localhost:5173'; - const frontendURL = `${req.protocol}://${req.get('host')}`; - const redirectURL = new URL('/auth/callback', frontendURL); - redirectURL.searchParams.append('access_token', tokens.access_token); - redirectURL.searchParams.append('refresh_token', tokens.refresh_token); - return res.redirect(redirectURL.toString()); - } + return res.redirect(redirectURL.toString()); + } + + @Get('kakao') + @UseGuards(PassportAuthGuard('kakao')) + async kakaoLogin() {} + + @Get('kakao/callback') + @UseGuards(PassportAuthGuard('kakao')) + async kakaoLoginCallback(@Request() req, @Res() res) { + const kakaoUser = req.user; + + const signUpDto: SignUpDto = { + name: kakaoUser.name, + email: kakaoUser.email, + provider: kakaoUser.provider, + providerId: kakaoUser.id, + isGuest: false, + }; + + const tokens = await this.authService.validateOAuthLogin(signUpDto); + + const origin = req.headers['origin']; + const frontendURL = + origin && origin.includes('localhost') + ? 'http://localhost:5173' + : 'https://www.corinee.site'; + const redirectURL = new URL('/auth/callback', frontendURL); + redirectURL.searchParams.append('access_token', tokens.access_token); + redirectURL.searchParams.append('refresh_token', tokens.refresh_token); + return res.redirect(redirectURL.toString()); + } @ApiResponse({ status: HttpStatus.OK, From 6e47f2caa39604e30d07c136f867e1613b126194 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Mon, 25 Nov 2024 13:58:47 +0900 Subject: [PATCH 24/59] =?UTF-8?q?feat:=20=EC=BD=94=EC=9D=B8=20=ED=98=84?= =?UTF-8?q?=EC=9E=AC=EA=B0=80=20rest=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/src/upbit/coin-list.service.ts | 19 +- packages/server/src/upbit/upbit.controller.ts | 165 +++++++++--------- 2 files changed, 103 insertions(+), 81 deletions(-) diff --git a/packages/server/src/upbit/coin-list.service.ts b/packages/server/src/upbit/coin-list.service.ts index 2553edea..37c4c3e5 100644 --- a/packages/server/src/upbit/coin-list.service.ts +++ b/packages/server/src/upbit/coin-list.service.ts @@ -34,15 +34,15 @@ export class CoinListService implements OnModuleInit { }); } async getSimpleCoin(coins) { - console.log(coins); + console.log(coins); let krwCoinInfo = this.coinDataUpdaterService.getKrwCoinInfo(); while (!krwCoinInfo) { await new Promise((resolve) => setTimeout(resolve, 100)); krwCoinInfo = this.coinDataUpdaterService.getKrwCoinInfo(); } - if (!coins.length) return []; - + if (!coins.length) return []; + return krwCoinInfo .filter((coin) => coins.includes(coin.market)) .map((coin) => { @@ -82,6 +82,19 @@ export class CoinListService implements OnModuleInit { .filter((coin) => coin.market.startsWith('USDT')); } + getCoinTickers(coins) { + const coinData = this.coinDataUpdaterService.getCoinLatestInfo(); + + const filteredData = Array.from(coinData.entries()) + .filter(([symbol]) => !coins || coins.includes(symbol)) + .map(([symbol, details]) => ({ + code: symbol, + ...details, + })); + + return filteredData; + } + convertToCodeCoinDto = (coin) => { coin.korean_name = this.coinDataUpdaterService .getCoinNameList() diff --git a/packages/server/src/upbit/upbit.controller.ts b/packages/server/src/upbit/upbit.controller.ts index cca4c634..422ae9e5 100644 --- a/packages/server/src/upbit/upbit.controller.ts +++ b/packages/server/src/upbit/upbit.controller.ts @@ -8,87 +8,96 @@ import { ApiQuery } from '@nestjs/swagger'; @Controller('upbit') export class UpbitController { - constructor( - private readonly sseService: SseService, - private readonly coinListService: CoinListService, - private readonly chartService : ChartService - ) {} + constructor( + private readonly sseService: SseService, + private readonly coinListService: CoinListService, + private readonly chartService: ChartService, + ) {} - @Sse('price-updates') - priceUpdates(@Query('coins') coins: string[]): Observable { - coins = coins || []; - const initData = this.sseService.initPriceStream( - coins, - this.coinListService.convertToMarketCoinDto, - ); - return concat( - initData, - this.sseService.getPriceUpdatesStream( - coins, - this.coinListService.convertToCodeCoinDto, - ), - ); - } - @Sse('orderbook') - orderbookUpdates(@Query('coins') coins: string[]): Observable { - coins = coins || []; - return this.sseService.getOrderbookUpdatesStream( - coins, - this.coinListService.convertToOrderbookDto, - ); - } + @Sse('price-updates') + priceUpdates(@Query('coins') coins: string[]): Observable { + coins = coins || []; + const initData = this.sseService.initPriceStream( + coins, + this.coinListService.convertToMarketCoinDto, + ); + return concat( + initData, + this.sseService.getPriceUpdatesStream( + coins, + this.coinListService.convertToCodeCoinDto, + ), + ); + } + @Sse('orderbook') + orderbookUpdates(@Query('coins') coins: string[]): Observable { + coins = coins || []; + return this.sseService.getOrderbookUpdatesStream( + coins, + this.coinListService.convertToOrderbookDto, + ); + } - @Get('market/all') - getAllMarkets() { - return this.coinListService.getAllCoinList(); - } - @Get('market/krw') - getKRWMarkets() { - return this.coinListService.getKRWCoinList(); - } - @Get('market/btc') - getBTCMarkets() { - return this.coinListService.getBTCCoinList(); - } - @Get('market/usdt') - getUSDTMarkets() { - return this.coinListService.getUSDTCoinList(); - } + @Get('market/all') + getAllMarkets() { + return this.coinListService.getAllCoinList(); + } + @Get('market/krw') + getKRWMarkets() { + return this.coinListService.getKRWCoinList(); + } + @Get('market/btc') + getBTCMarkets() { + return this.coinListService.getBTCCoinList(); + } + @Get('market/usdt') + getUSDTMarkets() { + return this.coinListService.getUSDTCoinList(); + } - @Get('market/top20-trade/krw') - getTop20TradeKRW() { - return this.coinListService.getMostTradeCoin(); - } - @Get('market/simplelist/krw') - getSomeKRW(@Query('market') market: string[]) { - const marketList = market || []; - return this.coinListService.getSimpleCoin(marketList); - } + @Get('market/top20-trade/krw') + getTop20TradeKRW() { + return this.coinListService.getMostTradeCoin(); + } + @Get('market/simplelist/krw') + getSomeKRW(@Query('market') market: string[]) { + const marketList = market || []; + return this.coinListService.getSimpleCoin(marketList); + } + @Get('market/tickers') + @ApiQuery({ name: 'coins', required: false, type: String }) + getCoinTickers(@Query('coins') coins?: string) { + return this.coinListService.getCoinTickers(coins); + } - @Get('candle/:type/:minute?') - @ApiQuery({ name: 'minute', required: false, type: String }) - async getCandle( - @Res() res: Response, - @Param('type') type : string, - @Query('market') market: string, - @Query('to') to:string, - @Param('minute') minute? :string - ){ - try{ - console.log("type : "+type) - console.log("market : "+market) - console.log("minute : "+minute) - console.log("to : "+to) - const response = await this.chartService.upbitApiDoor(type, market, to, minute) + @Get('candle/:type/:minute?') + @ApiQuery({ name: 'minute', required: false, type: String }) + async getCandle( + @Res() res: Response, + @Param('type') type: string, + @Query('market') market: string, + @Query('to') to: string, + @Param('minute') minute?: string, + ) { + try { + console.log('type : ' + type); + console.log('market : ' + market); + console.log('minute : ' + minute); + console.log('to : ' + to); + const response = await this.chartService.upbitApiDoor( + type, + market, + to, + minute, + ); - return res.status(response.statusCode).json(response) - }catch(error){ - console.error("error"+error) - return res.status(error.status) - .json({ - message: error.message || '서버오류입니다.', - error: error?.response || null, - });; - } - } + return res.status(response.statusCode).json(response); + } catch (error) { + console.error('error' + error); + return res.status(error.status).json({ + message: error.message || '서버오류입니다.', + error: error?.response || null, + }); + } + } } From eecc28bd63c12209763924682ecab07ad6975e41 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Mon, 25 Nov 2024 13:59:06 +0900 Subject: [PATCH 25/59] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=EC=9A=A9=20co?= =?UTF-8?q?mmit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 88e24d85..3a13ebc0 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -6,7 +6,7 @@ on: - main - dev - dev-be - - feature-be-#101 + - feature-be-#105 jobs: build_and_deploy: runs-on: ubuntu-latest From ee1febfe436f3a89bcf43b265dc2b065aab011d1 Mon Sep 17 00:00:00 2001 From: Seonghyeon0409 Date: Mon, 25 Nov 2024 14:25:40 +0900 Subject: [PATCH 26/59] =?UTF-8?q?chore:=20nginx=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nginx.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nginx.conf b/nginx.conf index 6d03ad5c..57e7f456 100644 --- a/nginx.conf +++ b/nginx.conf @@ -35,6 +35,9 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + # Origin 헤더 유지 + proxy_set_header Origin $http_origin; + # CORS 설정 추가 add_header 'Access-Control-Allow-Origin' 'https://www.corinee.site' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; From 03c1d1b8ba10a47caba63fd98f17bfca6e38bf83 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 18:49:12 +0900 Subject: [PATCH 27/59] =?UTF-8?q?refactor:=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/components/Header.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/client/src/components/Header.tsx b/packages/client/src/components/Header.tsx index b5c8e43e..2d60d898 100644 --- a/packages/client/src/components/Header.tsx +++ b/packages/client/src/components/Header.tsx @@ -63,11 +63,7 @@ function Header() { kakao_image )} From 9e650df050219e6db815e7e6017ff6415a991a79 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 18:49:23 +0900 Subject: [PATCH 28/59] =?UTF-8?q?refactor:=20sidebar=20css=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 --- packages/client/src/components/sidebar/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/sidebar/Sidebar.tsx b/packages/client/src/components/sidebar/Sidebar.tsx index 29e8404a..2ea0b963 100644 --- a/packages/client/src/components/sidebar/Sidebar.tsx +++ b/packages/client/src/components/sidebar/Sidebar.tsx @@ -45,7 +45,7 @@ function Sidebar() { ]; return ( -
+
{SIDEBAR_BUTTONS.map((button) => ( From 407d8f59e99d76f8b240ecc9a542c85ff2d8fca4 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 18:49:37 +0900 Subject: [PATCH 29/59] =?UTF-8?q?fix:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=20=EC=9A=94=EC=86=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/pages/home/components/CoinList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/pages/home/components/CoinList.tsx b/packages/client/src/pages/home/components/CoinList.tsx index 5a28d555..b5eea53e 100644 --- a/packages/client/src/pages/home/components/CoinList.tsx +++ b/packages/client/src/pages/home/components/CoinList.tsx @@ -21,7 +21,7 @@ function CoinList({ markets, activeCategory }: CoinListProps) { COINS_PER_PAGE * (currentScrollPage - 1), COINS_PER_PAGE * currentScrollPage, ), - [currentScrollPage], + [currentScrollPage, activeCategory], ); useEffect(() => { From cd0f112fe912d6bb15c0183d7a79d3503b14707f Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 18:49:55 +0900 Subject: [PATCH 30/59] =?UTF-8?q?refactor:=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/pages/layout/Layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/pages/layout/Layout.tsx b/packages/client/src/pages/layout/Layout.tsx index 57dc8686..5ff8066d 100644 --- a/packages/client/src/pages/layout/Layout.tsx +++ b/packages/client/src/pages/layout/Layout.tsx @@ -5,8 +5,8 @@ import { Outlet } from 'react-router-dom'; function Layout() { return ( <> -
-
+
+
From 0b633969052ded99ce9de573ead958a07085fc41 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 19:44:27 +0900 Subject: [PATCH 31/59] =?UTF-8?q?refactor:=20krw=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/pages/home/components/Coin.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/src/pages/home/components/Coin.tsx b/packages/client/src/pages/home/components/Coin.tsx index e30b14ba..d3f980b2 100644 --- a/packages/client/src/pages/home/components/Coin.tsx +++ b/packages/client/src/pages/home/components/Coin.tsx @@ -19,7 +19,8 @@ function Coin({ formatters, market, sseData }: CoinProps) { const { addRecentlyViewedMarket } = useRecentlyMarketStore(); const handleClick = () => { addRecentlyViewedMarket(market.market); - navigate(`/trade/${market.market}`); + console.log(market); + navigate(`/trade/KRW-${market.market.split('-')[1]}`); queryClient.invalidateQueries({ queryKey: ['recentlyMarketList'] }); }; const change: Change = sseData[market.market]?.change; From b5b2649c66c2f1e79fa70dd5c01197c6e38058cf Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 19:45:01 +0900 Subject: [PATCH 32/59] =?UTF-8?q?refactor:=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/pages/trade/components/order_book/OrderBook.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/pages/trade/components/order_book/OrderBook.tsx b/packages/client/src/pages/trade/components/order_book/OrderBook.tsx index 8bc893f1..8b1b08c3 100644 --- a/packages/client/src/pages/trade/components/order_book/OrderBook.tsx +++ b/packages/client/src/pages/trade/components/order_book/OrderBook.tsx @@ -29,7 +29,7 @@ function OrderBook({ const bids = formatBids(orderBook[market]); return ( -
+
호가
From 73c54b193eb0a169c716676c6b3cf49364b9c5f2 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 19:45:08 +0900 Subject: [PATCH 33/59] =?UTF-8?q?refactor:=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/trade/components/trade_footer/TradeFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx b/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx index 30a10600..06d03f37 100644 --- a/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx +++ b/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx @@ -9,7 +9,7 @@ function TradeFooter() { const duplicatedCoins = coins ? [...coins, ...coins, ...coins, ...coins] : []; return ( -
+
{duplicatedCoins?.map((coin, index) => ( From d9c174594f54d64ba7943298d8359065ef6ac1e0 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 21:37:24 +0900 Subject: [PATCH 34/59] =?UTF-8?q?feat:=20suspense=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/Router.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/client/src/Router.tsx b/packages/client/src/Router.tsx index 0e3bc8b7..848798ae 100644 --- a/packages/client/src/Router.tsx +++ b/packages/client/src/Router.tsx @@ -5,18 +5,21 @@ import Account from '@/pages/account/Account'; import Trade from '@/pages/trade/Trade'; import NotFound from '@/pages/not-found/NotFound'; import Redricet from '@/pages/auth/Redirect'; +import { Suspense } from 'react'; function Router() { return ( - - }> - } /> - } /> - } /> - - } /> - } /> - + + + }> + } /> + } /> + } /> + + } /> + } /> + + ); } From ac629a85263bf2203eba33d562fde749dc2914ec Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 21:38:00 +0900 Subject: [PATCH 35/59] =?UTF-8?q?refactor:=20ActiveLink=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 --- packages/client/src/components/Header.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/Header.tsx b/packages/client/src/components/Header.tsx index 2d60d898..6a56c5d9 100644 --- a/packages/client/src/components/Header.tsx +++ b/packages/client/src/components/Header.tsx @@ -1,6 +1,6 @@ import { useAuth } from '@/hooks/auth/useAuth'; import { Button, Navbar } from '@material-tailwind/react'; -import { Link } from 'react-router-dom'; +import { Link, NavLink } from 'react-router-dom'; import { useAuthStore } from '@/store/authStore'; import { useToast } from '@/hooks/ui/useToast'; import logoImage from '@asset/logo/corineeLogo.png'; @@ -32,12 +32,22 @@ function Header() {
- + + `${isActive ? 'text-black' : 'text-gray-600'} hover:text-black` + } + > 홈 - - + + + `${isActive ? 'text-black' : 'text-gray-600'} hover:text-black` + } + > 내 계좌 - +
{isAuthenticated ? ( From 1d36ddbeeedb4eb1ac377ab8175a53d1c66acce0 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 21:38:37 +0900 Subject: [PATCH 36/59] =?UTF-8?q?feat:=20=EC=9B=90=ED=99=94=20=EA=B1=B0?= =?UTF-8?q?=EB=9E=98=20=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=BD=94=EC=9D=B8?= =?UTF-8?q?=EC=9D=B8=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/hooks/market/useValidCoin.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/client/src/hooks/market/useValidCoin.ts diff --git a/packages/client/src/hooks/market/useValidCoin.ts b/packages/client/src/hooks/market/useValidCoin.ts new file mode 100644 index 00000000..2a683a32 --- /dev/null +++ b/packages/client/src/hooks/market/useValidCoin.ts @@ -0,0 +1,13 @@ +import { useMarketAll } from '@/hooks/market/useMarketAll'; +import { useMemo } from 'react'; +import { filterCoin } from '@/utility/filter'; +export function useValidCoin(market: string | undefined) { + const { data } = useMarketAll(); + const KRW_Markets = useMemo(() => filterCoin(data, 'KRW'), [data]); + const isValidCoin = useMemo(() => { + if (!market || !KRW_Markets) return false; + return KRW_Markets.some((item) => item.market === market); + }, [KRW_Markets]); + + return { isValidCoin }; +} From 4697c109ac1edf09efbbbc9365ce9744bde93e94 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 21:38:54 +0900 Subject: [PATCH 37/59] =?UTF-8?q?refactor:=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=20=EA=B8=80=EC=9E=90=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=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 --- packages/client/src/hooks/ui/useToast.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/client/src/hooks/ui/useToast.ts b/packages/client/src/hooks/ui/useToast.ts index f14ca586..40c57541 100644 --- a/packages/client/src/hooks/ui/useToast.ts +++ b/packages/client/src/hooks/ui/useToast.ts @@ -6,6 +6,12 @@ export function useToast() { pauseOnHover: false, hideProgressBar: true, transition: Zoom, + style: { + width: 'fit-content', + whiteSpace: 'nowrap', + display: 'inline-flex', + margin: '0 auto', + }, }; const showToast = { From b73a9316b6105a865ba848aba05396f825220c8a Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Mon, 25 Nov 2024 21:39:13 +0900 Subject: [PATCH 38/59] =?UTF-8?q?feat:=20=EC=9B=90=ED=99=94=20=EA=B1=B0?= =?UTF-8?q?=EB=9E=98=20=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=BD=94=EC=9D=B8?= =?UTF-8?q?=EC=9D=B8=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/pages/trade/Trade.tsx | 35 ++++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/client/src/pages/trade/Trade.tsx b/packages/client/src/pages/trade/Trade.tsx index 11601378..915dfc57 100644 --- a/packages/client/src/pages/trade/Trade.tsx +++ b/packages/client/src/pages/trade/Trade.tsx @@ -2,30 +2,45 @@ import Chart from '@/pages/trade/components/chart/Chart'; import OrderBook from '@/pages/trade/components/order_book/OrderBook'; import OrderForm from '@/pages/trade/components/order_form/OrderForm'; import TradeHeader from '@/pages/trade/components/trade_header/TradeHeader'; -import { useParams } from 'react-router-dom'; -import { useSSETicker } from '@/hooks/SSE/useSSETicker'; -import { Suspense, useMemo, useState } from 'react'; import ChartSkeleton from '@/pages/trade/components/chart/ChartSkeleton'; import TradeFooter from '@/pages/trade/components/trade_footer/TradeFooter'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useSSETicker } from '@/hooks/SSE/useSSETicker'; +import { Suspense, useMemo, useState, useCallback } from 'react'; +import { useToast } from '@/hooks/ui/useToast'; +import { useEffect } from 'react'; +import { useValidCoin } from '@/hooks/market/useValidCoin'; function Trade() { const { market } = useParams(); + const toast = useToast(); + const navigate = useNavigate(); + const marketCode = useMemo(() => (market ? [{ market }] : []), [market]); const { sseData: price } = useSSETicker(marketCode); const [selectPrice, setSelectPrice] = useState(null); + const { isValidCoin } = useValidCoin(market); - if (!market || !price) return; + useEffect(() => { + if (!isValidCoin) { + toast.error('원화로 거래 불가능한 코인이에요'); + navigate('/'); + } + }, [isValidCoin]); - const currentPrice = price[market]?.trade_price; - const handleSelectPrice = (price: number) => { + const handleSelectPrice = useCallback((price: number) => { setSelectPrice(price); - }; + }, []); + + if (!market || !price) return null; + const currentPrice = price[market]?.trade_price; + return ( -
+
-
+
}> - + Date: Mon, 25 Nov 2024 21:39:36 +0900 Subject: [PATCH 39/59] =?UTF-8?q?feat:=20=EC=B0=A8=ED=8A=B8=20=ED=98=84?= =?UTF-8?q?=EC=9E=AC=EA=B0=80=20=EB=B6=84=EB=B4=89=20=EB=93=B1=EB=9D=BD=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EA=B8=B0=EB=8A=A5=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/components/chart/CandleChart.tsx | 59 ++++++++++++++++++- .../pages/trade/components/chart/Chart.tsx | 10 +++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/client/src/pages/trade/components/chart/CandleChart.tsx b/packages/client/src/pages/trade/components/chart/CandleChart.tsx index 485eff3a..f01575a6 100644 --- a/packages/client/src/pages/trade/components/chart/CandleChart.tsx +++ b/packages/client/src/pages/trade/components/chart/CandleChart.tsx @@ -1,6 +1,6 @@ import { useRef, useEffect } from 'react'; -import { Candle, CandlePeriod } from '@/types/chart'; -import { IChartApi, ISeriesApi } from 'lightweight-charts'; +import { Candle, CandleFormat, CandlePeriod } from '@/types/chart'; +import { IChartApi, ISeriesApi, Time } from 'lightweight-charts'; import { initializeChart, setupCandlestickSeries, @@ -17,6 +17,7 @@ type CandleChartProps = { minute: number | undefined; data: Candle[]; fetchNextPage: () => Promise; + currentPrice: number; }; function CandleChart({ @@ -24,10 +25,47 @@ function CandleChart({ minute, data, fetchNextPage, + currentPrice, }: CandleChartProps) { const chartRef = useRef(null); const chartInstanceRef = useRef(null); const seriesRef = useRef | null>(null); + const lastCandleRef = useRef(null); + const intervalRef = useRef(null); + + const updateRealTimeCandle = () => { + if (!seriesRef.current || !minute) return; + + const now = new Date().getTime(); + const candlePeriodMs = (minute || 1) * 60 * 1000; + const currentCandleStartTime = ((Math.floor(now / candlePeriodMs) * + candlePeriodMs) / + 1000) as Time; + + if ( + !lastCandleRef.current || + lastCandleRef.current.time !== currentCandleStartTime + ) { + const newCandle = { + time: currentCandleStartTime, + open: currentPrice, + high: currentPrice, + low: currentPrice, + close: currentPrice, + }; + lastCandleRef.current = newCandle; + seriesRef.current.update(newCandle); + } else { + const updatedCandle = { + ...lastCandleRef.current, + close: currentPrice, + high: Math.max(lastCandleRef.current.high, currentPrice), + low: Math.min(lastCandleRef.current.low, currentPrice), + }; + lastCandleRef.current = updatedCandle; + seriesRef.current.update(updatedCandle); + } + }; useEffect(() => { if (!chartRef.current) return; @@ -50,6 +88,9 @@ function CandleChart({ resizeObserver.disconnect(); chartInstanceRef.current.remove(); } + if (intervalRef.current) { + clearInterval(intervalRef.current); + } }; }, []); @@ -57,6 +98,7 @@ function CandleChart({ if (!seriesRef.current || !chartInstanceRef.current) return; const formattedData = formatCandleData(data); seriesRef.current.setData(formattedData); + lastCandleRef.current = formattedData[formattedData.length - 1]; }, [data]); useEffect(() => { @@ -71,6 +113,19 @@ function CandleChart({ .subscribeVisibleLogicalRangeChange(handleScroll(fetchNextPage)); }, []); + useEffect(() => { + if (!seriesRef.current) return; + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + updateRealTimeCandle(); + intervalRef.current = setInterval(updateRealTimeCandle, 1000); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [currentPrice, minute]); return
; } diff --git a/packages/client/src/pages/trade/components/chart/Chart.tsx b/packages/client/src/pages/trade/components/chart/Chart.tsx index eba3bac2..77ce831b 100644 --- a/packages/client/src/pages/trade/components/chart/Chart.tsx +++ b/packages/client/src/pages/trade/components/chart/Chart.tsx @@ -4,7 +4,12 @@ import ChartSelector from '@/pages/trade/components/chart/ChartSelector'; import { CandlePeriod } from '@/types/chart'; import CandleChart from '@/pages/trade/components/chart/CandleChart'; -function Chart({ market }: { market: string }) { +type ChartProps = { + market: string; + currentPrice: number; +}; + +function Chart({ market, currentPrice }: ChartProps) { const [activePeriod, setActivePeriod] = useState('days'); const [minute, setMinute] = useState(); const { data, fetchNextPage } = usePeriodChart(market, activePeriod, minute); @@ -15,7 +20,7 @@ function Chart({ market }: { market: string }) { }; return ( -
+
); From b33a5405015bc99fc961bdc1d30802c836396b6d Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 10:12:00 +0900 Subject: [PATCH 40/59] =?UTF-8?q?refactor:=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EB=B0=B0=EC=9C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/pages/trade/components/chart/config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/client/src/pages/trade/components/chart/config.ts b/packages/client/src/pages/trade/components/chart/config.ts index 68817a1c..b2c77546 100644 --- a/packages/client/src/pages/trade/components/chart/config.ts +++ b/packages/client/src/pages/trade/components/chart/config.ts @@ -20,6 +20,10 @@ export const chartConfig = { vertLines: { color: '#1111' }, horzLines: { color: '#1111' }, }, + timeScale: { + rightOffset: 5, + barSpacing: 15, + }, }, candleStickOptions: { wickUpColor: 'rgb(225, 50, 85)', From a9e87a78ef6b855260782ad455ce3b8a1891731c Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 10:16:58 +0900 Subject: [PATCH 41/59] =?UTF-8?q?refactor:=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EB=B0=B0=EC=9C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/pages/trade/components/chart/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/pages/trade/components/chart/config.ts b/packages/client/src/pages/trade/components/chart/config.ts index b2c77546..754e3132 100644 --- a/packages/client/src/pages/trade/components/chart/config.ts +++ b/packages/client/src/pages/trade/components/chart/config.ts @@ -22,7 +22,7 @@ export const chartConfig = { }, timeScale: { rightOffset: 5, - barSpacing: 15, + barSpacing: 10, }, }, candleStickOptions: { From 0bc969fb14b25c03b57c50c96c8618071f53f90b Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 10:31:13 +0900 Subject: [PATCH 42/59] =?UTF-8?q?feat:=20=EB=B6=84=EB=B4=89=20=EC=99=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=8B=9C=EA=B0=84=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B0=80=EA=B2=A9=20=EC=B0=A8=ED=8A=B8=20=EB=93=B1?= =?UTF-8?q?=EB=9D=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/components/chart/CandleChart.tsx | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/client/src/pages/trade/components/chart/CandleChart.tsx b/packages/client/src/pages/trade/components/chart/CandleChart.tsx index f01575a6..a705ca10 100644 --- a/packages/client/src/pages/trade/components/chart/CandleChart.tsx +++ b/packages/client/src/pages/trade/components/chart/CandleChart.tsx @@ -33,14 +33,59 @@ function CandleChart({ const lastCandleRef = useRef(null); const intervalRef = useRef(null); + const getPeriodMs = () => { + switch (activePeriod) { + case 'minutes': + return (minute || 1) * 60 * 1000; + case 'days': + return 24 * 60 * 60 * 1000; + case 'weeks': + return 7 * 24 * 60 * 60 * 1000; + case 'months': + return 30 * 24 * 60 * 60 * 1000; + default: + return 60 * 1000; + } + }; + + const getCurrentCandleStartTime = () => { + const now = new Date(); + const periodMs = getPeriodMs(); + + switch (activePeriod) { + case 'minutes': + return ((Math.floor(now.getTime() / periodMs) * periodMs) / + 1000) as Time; + + case 'days': + const startOfDay = new Date(now); + startOfDay.setUTCHours(0, 0, 0, 0); + return (startOfDay.getTime() / 1000) as Time; + + case 'weeks': + const startOfWeek = new Date(now); + startOfWeek.setUTCHours(0, 0, 0, 0); + const day = startOfWeek.getUTCDay(); + const diff = startOfWeek.getUTCDate() - day + (day === 0 ? -6 : 1); + startOfWeek.setUTCDate(diff); + return (startOfWeek.getTime() / 1000) as Time; + + case 'months': + const startOfMonth = new Date(now); + startOfMonth.setUTCHours(0, 0, 0, 0); + startOfMonth.setUTCDate(1); + return (startOfMonth.getTime() / 1000) as Time; + + default: + return ((Math.floor(now.getTime() / periodMs) * periodMs) / + 1000) as Time; + } + }; + const updateRealTimeCandle = () => { - if (!seriesRef.current || !minute) return; + if (!seriesRef.current) return; - const now = new Date().getTime(); - const candlePeriodMs = (minute || 1) * 60 * 1000; - const currentCandleStartTime = ((Math.floor(now / candlePeriodMs) * - candlePeriodMs) / - 1000) as Time; + const currentCandleStartTime = getCurrentCandleStartTime(); if ( !lastCandleRef.current || @@ -115,11 +160,16 @@ function CandleChart({ useEffect(() => { if (!seriesRef.current) return; + if (intervalRef.current) { clearInterval(intervalRef.current); } + updateRealTimeCandle(); - intervalRef.current = setInterval(updateRealTimeCandle, 1000); + + const updateInterval = activePeriod === 'minutes' ? 1000 : 5000; + intervalRef.current = setInterval(updateRealTimeCandle, updateInterval); + return () => { if (intervalRef.current) { clearInterval(intervalRef.current); From eb677237d57500060c8bf343797198b73847c225 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 14:31:50 +0900 Subject: [PATCH 43/59] =?UTF-8?q?refactor:=20=EB=AF=B8=EA=B5=AD=20?= =?UTF-8?q?=EC=8B=9C=EC=B0=A8=20=EA=B3=84=EC=82=B0=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=95=9C=EA=B5=AD=20=EC=8B=9C=EA=B0=84=EC=9C=BC=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 --- .../src/utility/format/formatCandleData.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/client/src/utility/format/formatCandleData.ts b/packages/client/src/utility/format/formatCandleData.ts index 403c9c1b..cda0aeb5 100644 --- a/packages/client/src/utility/format/formatCandleData.ts +++ b/packages/client/src/utility/format/formatCandleData.ts @@ -1,10 +1,10 @@ -import { Candle, CandleFormat } from '@/types/chart'; import { Time } from 'lightweight-charts'; - +import { Candle, CandleFormat } from '@/types/chart'; export function formatCandleData(data: Candle[]): CandleFormat[] { const uniqueData = data.reduce( (acc, current) => { - const timeKey = new Date(current.candle_date_time_kst).getTime(); + const date = new Date(current.candle_date_time_kst); + const timeKey = date.getTime() + 9 * 60 * 60 * 1000; acc[timeKey] = current; return acc; }, @@ -12,13 +12,17 @@ export function formatCandleData(data: Candle[]): CandleFormat[] { ); const sortedData = Object.values(uniqueData).sort((a, b) => { - const dateA = new Date(a.candle_date_time_kst).getTime(); - const dateB = new Date(b.candle_date_time_kst).getTime(); + const dateA = + new Date(a.candle_date_time_kst).getTime() + 9 * 60 * 60 * 1000; + const dateB = + new Date(b.candle_date_time_kst).getTime() + 9 * 60 * 60 * 1000; return dateA - dateB; }); const formattedData = sortedData.map((candle) => ({ - time: (new Date(candle.candle_date_time_kst).getTime() / 1000) as Time, + time: ((new Date(candle.candle_date_time_kst).getTime() + + 9 * 60 * 60 * 1000) / + 1000) as Time, open: candle.opening_price, high: candle.high_price, low: candle.low_price, From fb9b79e99e539e599492408d7196b844b93c1279 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 14:32:04 +0900 Subject: [PATCH 44/59] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/types/account.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client/src/types/account.ts b/packages/client/src/types/account.ts index 99299e3a..d7f79723 100644 --- a/packages/client/src/types/account.ts +++ b/packages/client/src/types/account.ts @@ -11,4 +11,5 @@ export type AccountCoin = { quantity: number; price: number; averagePrice: number; + availableQuantity: number; }; From 076116f0afc6227ea3d1c3db1447c6f5c2f351c2 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 14:32:08 +0900 Subject: [PATCH 45/59] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/trade/components/order_form/OrderSellForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx index b1a857ae..bdbc771d 100644 --- a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx +++ b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx @@ -63,7 +63,7 @@ function OrderSellForm({ currentPrice, selectPrice }: OrderSellFormProps) { label="수량" value={quantity} onChange={setQuantity} - placeholder={`최대 ${targetCoin?.quantity}개 가능`} + placeholder={`최대 ${targetCoin?.availableQuantity}개 가능`} errorMessage={quantityErrorMessage} /> Date: Tue, 26 Nov 2024 15:51:35 +0900 Subject: [PATCH 46/59] =?UTF-8?q?chore:=20utils=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=93=A4=20=ED=8F=B4=EB=8D=94=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/utility/{ => api}/queryString.ts | 0 .../chart/chartEvent.ts | 0 .../chart/chartSetup.ts | 2 +- .../src/utility/chart/chartTimeUtils.ts | 55 +++++++++++++++++++ .../components => utility}/chart/config.ts | 0 .../utility/{ => finance}/calculateProfit.ts | 0 .../calculateTotalPrice.ts} | 0 .../{ => finance}/portfolioEvaluator.ts | 0 .../src/utility/{ => storage}/cookies.ts | 0 .../utility/{ => storage}/recentlyMarket.ts | 0 .../src/utility/{ => validation}/filter.ts | 0 .../src/utility/{ => validation}/typeGuard.ts | 0 12 files changed, 56 insertions(+), 1 deletion(-) rename packages/client/src/utility/{ => api}/queryString.ts (100%) rename packages/client/src/{pages/trade/components => utility}/chart/chartEvent.ts (100%) rename packages/client/src/{pages/trade/components => utility}/chart/chartSetup.ts (92%) create mode 100644 packages/client/src/utility/chart/chartTimeUtils.ts rename packages/client/src/{pages/trade/components => utility}/chart/config.ts (100%) rename packages/client/src/utility/{ => finance}/calculateProfit.ts (100%) rename packages/client/src/utility/{order.ts => finance/calculateTotalPrice.ts} (100%) rename packages/client/src/utility/{ => finance}/portfolioEvaluator.ts (100%) rename packages/client/src/utility/{ => storage}/cookies.ts (100%) rename packages/client/src/utility/{ => storage}/recentlyMarket.ts (100%) rename packages/client/src/utility/{ => validation}/filter.ts (100%) rename packages/client/src/utility/{ => validation}/typeGuard.ts (100%) diff --git a/packages/client/src/utility/queryString.ts b/packages/client/src/utility/api/queryString.ts similarity index 100% rename from packages/client/src/utility/queryString.ts rename to packages/client/src/utility/api/queryString.ts diff --git a/packages/client/src/pages/trade/components/chart/chartEvent.ts b/packages/client/src/utility/chart/chartEvent.ts similarity index 100% rename from packages/client/src/pages/trade/components/chart/chartEvent.ts rename to packages/client/src/utility/chart/chartEvent.ts diff --git a/packages/client/src/pages/trade/components/chart/chartSetup.ts b/packages/client/src/utility/chart/chartSetup.ts similarity index 92% rename from packages/client/src/pages/trade/components/chart/chartSetup.ts rename to packages/client/src/utility/chart/chartSetup.ts index 3b997be6..05f10784 100644 --- a/packages/client/src/pages/trade/components/chart/chartSetup.ts +++ b/packages/client/src/utility/chart/chartSetup.ts @@ -1,7 +1,7 @@ // src/components/Chart/utils/chartSetup.ts import { IChartApi, createChart, ISeriesApi } from 'lightweight-charts'; import { CandleFormat } from '@/types/chart'; -import { ChartConfig } from '@/pages/trade/components/chart/config'; +import { ChartConfig } from '@/utility/chart/config'; export const initializeChart = ( container: HTMLElement, diff --git a/packages/client/src/utility/chart/chartTimeUtils.ts b/packages/client/src/utility/chart/chartTimeUtils.ts new file mode 100644 index 00000000..c070ddc3 --- /dev/null +++ b/packages/client/src/utility/chart/chartTimeUtils.ts @@ -0,0 +1,55 @@ +import { CandlePeriod } from '@/types/chart'; +import { Time } from 'lightweight-charts'; + +export const getPeriodMs = (activePeriod: CandlePeriod, minute?: number) => { + switch (activePeriod) { + case 'minutes': + return (minute || 1) * 60 * 1000; + case 'days': + return 24 * 60 * 60 * 1000; + case 'weeks': + return 7 * 24 * 60 * 60 * 1000; + case 'months': + return 30 * 24 * 60 * 60 * 1000; + default: + return 60 * 1000; + } +}; + +export const getCurrentCandleStartTime = ( + activePeriod: CandlePeriod, + minute?: number, +) => { + const now = new Date(); + const periodMs = getPeriodMs(activePeriod, minute); + + switch (activePeriod) { + case 'minutes': + return ((Math.floor(now.getTime() / periodMs) * periodMs) / 1000) as Time; + + case 'days': { + const startOfDay = new Date(now); + startOfDay.setUTCHours(0, 0, 0, 0); + return (startOfDay.getTime() / 1000) as Time; + } + + case 'weeks': { + const startOfWeek = new Date(now); + startOfWeek.setUTCHours(0, 0, 0, 0); + const day = startOfWeek.getUTCDay(); + const diff = startOfWeek.getUTCDate() - day + (day === 0 ? -6 : 1); + startOfWeek.setUTCDate(diff); + return (startOfWeek.getTime() / 1000) as Time; + } + + case 'months': { + const startOfMonth = new Date(now); + startOfMonth.setUTCHours(0, 0, 0, 0); + startOfMonth.setUTCDate(1); + return (startOfMonth.getTime() / 1000) as Time; + } + + default: + return ((Math.floor(now.getTime() / periodMs) * periodMs) / 1000) as Time; + } +}; diff --git a/packages/client/src/pages/trade/components/chart/config.ts b/packages/client/src/utility/chart/config.ts similarity index 100% rename from packages/client/src/pages/trade/components/chart/config.ts rename to packages/client/src/utility/chart/config.ts diff --git a/packages/client/src/utility/calculateProfit.ts b/packages/client/src/utility/finance/calculateProfit.ts similarity index 100% rename from packages/client/src/utility/calculateProfit.ts rename to packages/client/src/utility/finance/calculateProfit.ts diff --git a/packages/client/src/utility/order.ts b/packages/client/src/utility/finance/calculateTotalPrice.ts similarity index 100% rename from packages/client/src/utility/order.ts rename to packages/client/src/utility/finance/calculateTotalPrice.ts diff --git a/packages/client/src/utility/portfolioEvaluator.ts b/packages/client/src/utility/finance/portfolioEvaluator.ts similarity index 100% rename from packages/client/src/utility/portfolioEvaluator.ts rename to packages/client/src/utility/finance/portfolioEvaluator.ts diff --git a/packages/client/src/utility/cookies.ts b/packages/client/src/utility/storage/cookies.ts similarity index 100% rename from packages/client/src/utility/cookies.ts rename to packages/client/src/utility/storage/cookies.ts diff --git a/packages/client/src/utility/recentlyMarket.ts b/packages/client/src/utility/storage/recentlyMarket.ts similarity index 100% rename from packages/client/src/utility/recentlyMarket.ts rename to packages/client/src/utility/storage/recentlyMarket.ts diff --git a/packages/client/src/utility/filter.ts b/packages/client/src/utility/validation/filter.ts similarity index 100% rename from packages/client/src/utility/filter.ts rename to packages/client/src/utility/validation/filter.ts diff --git a/packages/client/src/utility/typeGuard.ts b/packages/client/src/utility/validation/typeGuard.ts similarity index 100% rename from packages/client/src/utility/typeGuard.ts rename to packages/client/src/utility/validation/typeGuard.ts From bc5bcaed300863cfd93aa92830ed81d381f61910 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:53:08 +0900 Subject: [PATCH 47/59] =?UTF-8?q?chore:=20utlitiy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/api/interceptors.ts | 2 +- packages/client/src/components/sidebar/RecentlyViewed.tsx | 2 +- packages/client/src/hooks/auth/useAuth.ts | 2 +- packages/client/src/hooks/useMyHistory.ts | 5 ++--- packages/client/src/pages/account/balance/BalanceCoin.tsx | 2 +- packages/client/src/pages/account/balance/BalanceTable.tsx | 2 +- packages/client/src/pages/auth/Redirect.tsx | 2 +- packages/client/src/pages/home/components/CoinView.tsx | 4 ++-- packages/client/src/store/recentlyViewed.ts | 2 +- 9 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/client/src/api/interceptors.ts b/packages/client/src/api/interceptors.ts index 09ebd396..44977eb3 100644 --- a/packages/client/src/api/interceptors.ts +++ b/packages/client/src/api/interceptors.ts @@ -1,6 +1,6 @@ import { authInstance, instance } from '@/api/instance'; import { Login } from '@/types/auth'; -import { getCookie, removeCookie, setCookie } from '@/utility/cookies'; +import { getCookie, removeCookie, setCookie } from '@/utility/storage/cookies'; import { AxiosResponse } from 'axios'; authInstance.interceptors.request.use( diff --git a/packages/client/src/components/sidebar/RecentlyViewed.tsx b/packages/client/src/components/sidebar/RecentlyViewed.tsx index 1beb68fc..e05a438f 100644 --- a/packages/client/src/components/sidebar/RecentlyViewed.tsx +++ b/packages/client/src/components/sidebar/RecentlyViewed.tsx @@ -3,7 +3,7 @@ import { useSSETicker } from '@/hooks/SSE/useSSETicker'; import { formatData } from '@/utility/format/formatSSEData'; import useRecentlyMarketStore from '@/store/recentlyViewed'; import { useRecentlyMarketList } from '@/hooks/market/useRecentlyMarket'; -import { convertToQueryString } from '@/utility/queryString'; +import { convertToQueryString } from '@/utility/api/queryString'; import { SidebarMarketData } from '@/types/market'; function RecentlyViewed() { diff --git a/packages/client/src/hooks/auth/useAuth.ts b/packages/client/src/hooks/auth/useAuth.ts index 0d6473e6..c7170651 100644 --- a/packages/client/src/hooks/auth/useAuth.ts +++ b/packages/client/src/hooks/auth/useAuth.ts @@ -1,7 +1,7 @@ import { guestLogin, logout } from '@/api/auth'; import { useAuthStore } from '@/store/authStore'; import { Login } from '@/types/auth'; -import { removeCookie, setCookie } from '@/utility/cookies'; +import { removeCookie, setCookie } from '@/utility/storage/cookies'; import { useMutation } from '@tanstack/react-query'; export function useAuth() { diff --git a/packages/client/src/hooks/useMyHistory.ts b/packages/client/src/hooks/useMyHistory.ts index 515ef262..1718422e 100644 --- a/packages/client/src/hooks/useMyHistory.ts +++ b/packages/client/src/hooks/useMyHistory.ts @@ -1,16 +1,15 @@ import { myHistory } from '@/api/history'; -import { getCookie } from '@/utility/cookies'; +import { getCookie } from '@/utility/storage/cookies'; import { useSuspenseQuery } from '@tanstack/react-query'; export function useMyHistory() { const QUERY_KEY = 'MY_History'; const token = getCookie('access_token'); - const {data} = useSuspenseQuery({ + const { data } = useSuspenseQuery({ queryFn: () => myHistory(token), queryKey: [QUERY_KEY], refetchOnMount: 'always', }); - return data; } diff --git a/packages/client/src/pages/account/balance/BalanceCoin.tsx b/packages/client/src/pages/account/balance/BalanceCoin.tsx index e60c9c24..da8088d4 100644 --- a/packages/client/src/pages/account/balance/BalanceCoin.tsx +++ b/packages/client/src/pages/account/balance/BalanceCoin.tsx @@ -1,7 +1,7 @@ import { Change, CoinTicker } from '@/types/ticker'; import colorClasses from '@/constants/priceColor'; import { AccountCoin } from '@/types/account'; -import PORTFOLIO_EVALUATOR from '@/utility/portfolioEvaluator'; +import PORTFOLIO_EVALUATOR from '@/utility/finance/portfolioEvaluator'; import { Link } from 'react-router-dom'; type BalanceCoinProps = { diff --git a/packages/client/src/pages/account/balance/BalanceTable.tsx b/packages/client/src/pages/account/balance/BalanceTable.tsx index ae27f771..2855cc97 100644 --- a/packages/client/src/pages/account/balance/BalanceTable.tsx +++ b/packages/client/src/pages/account/balance/BalanceTable.tsx @@ -1,7 +1,7 @@ import BalanceInfo from '@/pages/account/balance/BalanceInfo'; import { BalanceMarket } from '@/types/market'; import { SSEDataType } from '@/types/ticker'; -import PORTFOLIO_EVALUATOR from '@/utility/portfolioEvaluator'; +import PORTFOLIO_EVALUATOR from '@/utility/finance/portfolioEvaluator'; type BalanceTableProps = { KRW: number; diff --git a/packages/client/src/pages/auth/Redirect.tsx b/packages/client/src/pages/auth/Redirect.tsx index 83cfdb82..45631956 100644 --- a/packages/client/src/pages/auth/Redirect.tsx +++ b/packages/client/src/pages/auth/Redirect.tsx @@ -1,6 +1,6 @@ import { useSearchParams, useNavigate } from 'react-router-dom'; import { useEffect } from 'react'; -import { setCookie } from '@/utility/cookies'; +import { setCookie } from '@/utility/storage/cookies'; import { useAuthStore } from '@/store/authStore'; function Redirect() { diff --git a/packages/client/src/pages/home/components/CoinView.tsx b/packages/client/src/pages/home/components/CoinView.tsx index 9cedb0c8..6a676ca7 100644 --- a/packages/client/src/pages/home/components/CoinView.tsx +++ b/packages/client/src/pages/home/components/CoinView.tsx @@ -3,8 +3,8 @@ import CoinCategories from '@/pages/home/components/CoinCategories'; import CoinList from '@/pages/home/components/CoinList'; import { MarketData } from '@/types/market'; import { MarketCategory } from '@/types/category'; -import { filterCoin } from '@/utility/filter'; -import { isMarket } from '@/utility/typeGuard'; +import { filterCoin } from '@/utility/validation/filter'; +import { isMarket } from '@/utility/validation/typeGuard'; import { useState } from 'react'; function CoinView() { diff --git a/packages/client/src/store/recentlyViewed.ts b/packages/client/src/store/recentlyViewed.ts index f4d272cf..7acb3b99 100644 --- a/packages/client/src/store/recentlyViewed.ts +++ b/packages/client/src/store/recentlyViewed.ts @@ -1,7 +1,7 @@ import { getRecentlyViewedMarketList, setRecentlyViewedMarketList, -} from '@/utility/recentlyMarket'; +} from '@/utility/storage/recentlyMarket'; import { create } from 'zustand'; type RecentlyMarketStore = { From 0b9fc1838ecf4d915b9b1f96f79d8dd1bc8b6ddf Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:53:33 +0900 Subject: [PATCH 48/59] =?UTF-8?q?refactor:=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/hooks/chart/useChartSetup.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/client/src/hooks/chart/useChartSetup.ts diff --git a/packages/client/src/hooks/chart/useChartSetup.ts b/packages/client/src/hooks/chart/useChartSetup.ts new file mode 100644 index 00000000..e6bebc17 --- /dev/null +++ b/packages/client/src/hooks/chart/useChartSetup.ts @@ -0,0 +1,40 @@ +import { + initializeChart, + setupCandlestickSeries, +} from '@/utility/chart/chartSetup'; +import { useRef, useEffect } from 'react'; +import { IChartApi, ISeriesApi } from 'lightweight-charts'; +import { chartConfig } from '@/utility/chart/config'; +import { handleResize } from '@/utility/chart/chartEvent'; + +export function useChartSetup() { + const chartRef = useRef(null); + const chartInstanceRef = useRef(null); + const seriesRef = useRef | null>(null); + + useEffect(() => { + if (!chartRef.current) return; + chartInstanceRef.current = initializeChart(chartRef.current, chartConfig); + seriesRef.current = setupCandlestickSeries( + chartInstanceRef.current, + [], + chartConfig, + ); + const resizeObserver = new ResizeObserver(() => { + handleResize(chartRef, chartInstanceRef); + }); + + if (chartRef.current.parentElement) { + resizeObserver.observe(chartRef.current.parentElement); + } + + return () => { + if (chartInstanceRef.current) { + resizeObserver.disconnect(); + chartInstanceRef.current.remove(); + } + }; + }, []); + + return { chartRef, chartInstanceRef, seriesRef }; +} From 204f850893789ba79dbec8946b0393d5b765bf20 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:54:47 +0900 Subject: [PATCH 49/59] =?UTF-8?q?refactor:=20market=20->chart=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A1=9C=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/hooks/chart/usePeriodChart.ts | 40 +++++++++++++++++++ .../client/src/hooks/market/usePeriodChart.ts | 38 ------------------ 2 files changed, 40 insertions(+), 38 deletions(-) create mode 100644 packages/client/src/hooks/chart/usePeriodChart.ts delete mode 100644 packages/client/src/hooks/market/usePeriodChart.ts diff --git a/packages/client/src/hooks/chart/usePeriodChart.ts b/packages/client/src/hooks/chart/usePeriodChart.ts new file mode 100644 index 00000000..398c5f92 --- /dev/null +++ b/packages/client/src/hooks/chart/usePeriodChart.ts @@ -0,0 +1,40 @@ +import { getCandleByPeriod } from '@/api/market'; +import { Candle, CandlePeriod, InfiniteCandle } from '@/types/chart'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; + +export function usePeriodChart( + market: string, + period: CandlePeriod, + minute?: number, +) { + const { data, fetchNextPage, hasNextPage, refetch } = + useSuspenseInfiniteQuery< + Candle[], + Error, + InfiniteCandle, + [string, string, CandlePeriod, number?], + string | undefined + >({ + queryKey: ['candles', market, period, minute], + queryFn: ({ pageParam }) => { + return getCandleByPeriod(market, period, pageParam, minute); + }, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) return undefined; + const oldestCandle = lastPage[lastPage.length - 1]; + return oldestCandle?.candle_date_time_utc; + }, + initialPageParam: undefined, + select: (data) => ({ + candles: data.pages.flat(), + hasNextPage: data.pages[data.pages.length - 1]?.length === 200, + }), + }); + + return { + refetch, + data, + fetchNextPage, + hasNextPage, + }; +} diff --git a/packages/client/src/hooks/market/usePeriodChart.ts b/packages/client/src/hooks/market/usePeriodChart.ts deleted file mode 100644 index 4a149341..00000000 --- a/packages/client/src/hooks/market/usePeriodChart.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getCandleByPeriod } from '@/api/market'; -import { Candle, CandlePeriod, InfiniteCandle } from '@/types/chart'; -import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; - -export function usePeriodChart( - market: string, - period: CandlePeriod, - minute?: number, -) { - const { data, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery< - Candle[], - Error, - InfiniteCandle, - [string, string, CandlePeriod, number?], - string | undefined - >({ - queryKey: ['candles', market, period, minute], - queryFn: ({ pageParam }) => { - return getCandleByPeriod(market, period, pageParam, minute); - }, - getNextPageParam: (lastPage) => { - if (lastPage.length === 0) return undefined; - const oldestCandle = lastPage[lastPage.length - 1]; - return oldestCandle?.candle_date_time_utc; - }, - initialPageParam: undefined, - select: (data) => ({ - candles: data.pages.flat(), - hasNextPage: data.pages[data.pages.length - 1]?.length === 200, - }), - }); - - return { - data, - fetchNextPage, - hasNextPage, - }; -} From 7ee7bdbc61291ab48889d95eded76bab355b5546 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:55:05 +0900 Subject: [PATCH 50/59] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EB=93=B1=EB=9D=BD=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/chart/useRealTimeCandle.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/client/src/hooks/chart/useRealTimeCandle.ts diff --git a/packages/client/src/hooks/chart/useRealTimeCandle.ts b/packages/client/src/hooks/chart/useRealTimeCandle.ts new file mode 100644 index 00000000..70808099 --- /dev/null +++ b/packages/client/src/hooks/chart/useRealTimeCandle.ts @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react'; +import { CandleFormat, CandlePeriod } from '@/types/chart'; +import { ISeriesApi } from 'lightweight-charts'; +import { getCurrentCandleStartTime } from '@/utility/chart/chartTimeUtils'; + +type Props = { + seriesRef: React.RefObject>; + currentPrice: number; + activePeriod: CandlePeriod; + refetch: () => Promise; + minute?: number; +}; +export function useRealTimeCandle({ + seriesRef, + currentPrice, + activePeriod, + refetch, + minute, +}: Props) { + const lastCandleRef = useRef(null); + + const updateRealTimeCandle = () => { + if (!seriesRef.current || !currentPrice || !lastCandleRef.current) return; + + const currentCandleStartTime = getCurrentCandleStartTime( + activePeriod, + minute, + ); + if ( + !lastCandleRef.current || + lastCandleRef.current.time !== currentCandleStartTime + ) { + refetch(); + } else { + const updatedCandle = { + ...lastCandleRef.current, + close: currentPrice, + high: Math.max(lastCandleRef.current.high, currentPrice), + low: Math.min(lastCandleRef.current.low, currentPrice), + }; + lastCandleRef.current = updatedCandle; + seriesRef.current.update(updatedCandle); + } + }; + + useEffect(() => { + if (!seriesRef.current || !currentPrice) return; + updateRealTimeCandle(); + }, [currentPrice, minute, activePeriod]); + + return { lastCandleRef }; +} From e7c357804b52958c13df5ecba1a3cb66340e4306 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:55:29 +0900 Subject: [PATCH 51/59] =?UTF-8?q?chore:=20utlitiy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/hooks/market/useValidCoin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/hooks/market/useValidCoin.ts b/packages/client/src/hooks/market/useValidCoin.ts index 2a683a32..321117dd 100644 --- a/packages/client/src/hooks/market/useValidCoin.ts +++ b/packages/client/src/hooks/market/useValidCoin.ts @@ -1,6 +1,6 @@ import { useMarketAll } from '@/hooks/market/useMarketAll'; import { useMemo } from 'react'; -import { filterCoin } from '@/utility/filter'; +import { filterCoin } from '@/utility/validation/filter'; export function useValidCoin(market: string | undefined) { const { data } = useMarketAll(); const KRW_Markets = useMemo(() => filterCoin(data, 'KRW'), [data]); From de357585f829ac06664d38f4947b538df17dcfaa Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:55:36 +0900 Subject: [PATCH 52/59] =?UTF-8?q?chore:=20utlitiy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/hooks/ui/useSideDraw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/hooks/ui/useSideDraw.ts b/packages/client/src/hooks/ui/useSideDraw.ts index 383e5e47..38cce56a 100644 --- a/packages/client/src/hooks/ui/useSideDraw.ts +++ b/packages/client/src/hooks/ui/useSideDraw.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { SideBarCategory } from '@/types/category'; -import { isSideBarMenu } from '@/utility/typeGuard'; +import { isSideBarMenu } from '@/utility/validation/typeGuard'; function useSideDrawer() { const [activeMenu, setActiveMenu] = useState(null); From fe6b2a61464fe9008aeab1cc33589eed0751e978 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:55:59 +0900 Subject: [PATCH 53/59] =?UTF-8?q?refactor:=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=A3=BC=EC=9A=94=20=EB=A1=9C=EC=A7=81=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/components/chart/CandleChart.tsx | 156 ++---------------- 1 file changed, 15 insertions(+), 141 deletions(-) diff --git a/packages/client/src/pages/trade/components/chart/CandleChart.tsx b/packages/client/src/pages/trade/components/chart/CandleChart.tsx index a705ca10..3935ab57 100644 --- a/packages/client/src/pages/trade/components/chart/CandleChart.tsx +++ b/packages/client/src/pages/trade/components/chart/CandleChart.tsx @@ -1,21 +1,15 @@ -import { useRef, useEffect } from 'react'; -import { Candle, CandleFormat, CandlePeriod } from '@/types/chart'; -import { IChartApi, ISeriesApi, Time } from 'lightweight-charts'; -import { - initializeChart, - setupCandlestickSeries, -} from '@/pages/trade/components/chart/chartSetup'; -import { chartConfig } from '@/pages/trade/components/chart/config'; +import { useEffect } from 'react'; +import { Candle, CandlePeriod } from '@/types/chart'; import { formatCandleData } from '@/utility/format/formatCandleData'; -import { - handleResize, - handleScroll, -} from '@/pages/trade/components/chart/chartEvent'; +import { handleScroll } from '@/utility/chart/chartEvent'; +import { useRealTimeCandle } from '@/hooks/chart/useRealTimeCandle'; +import { useChartSetup } from '@/hooks/chart/useChartSetup'; type CandleChartProps = { activePeriod: CandlePeriod; minute: number | undefined; data: Candle[]; + refetch: () => Promise; fetchNextPage: () => Promise; currentPrice: number; }; @@ -24,120 +18,18 @@ function CandleChart({ activePeriod, minute, data, + refetch, fetchNextPage, currentPrice, }: CandleChartProps) { - const chartRef = useRef(null); - const chartInstanceRef = useRef(null); - const seriesRef = useRef | null>(null); - const lastCandleRef = useRef(null); - const intervalRef = useRef(null); - - const getPeriodMs = () => { - switch (activePeriod) { - case 'minutes': - return (minute || 1) * 60 * 1000; - case 'days': - return 24 * 60 * 60 * 1000; - case 'weeks': - return 7 * 24 * 60 * 60 * 1000; - case 'months': - return 30 * 24 * 60 * 60 * 1000; - default: - return 60 * 1000; - } - }; - - const getCurrentCandleStartTime = () => { - const now = new Date(); - const periodMs = getPeriodMs(); - - switch (activePeriod) { - case 'minutes': - return ((Math.floor(now.getTime() / periodMs) * periodMs) / - 1000) as Time; - - case 'days': - const startOfDay = new Date(now); - startOfDay.setUTCHours(0, 0, 0, 0); - return (startOfDay.getTime() / 1000) as Time; - - case 'weeks': - const startOfWeek = new Date(now); - startOfWeek.setUTCHours(0, 0, 0, 0); - const day = startOfWeek.getUTCDay(); - const diff = startOfWeek.getUTCDate() - day + (day === 0 ? -6 : 1); - startOfWeek.setUTCDate(diff); - return (startOfWeek.getTime() / 1000) as Time; - - case 'months': - const startOfMonth = new Date(now); - startOfMonth.setUTCHours(0, 0, 0, 0); - startOfMonth.setUTCDate(1); - return (startOfMonth.getTime() / 1000) as Time; - - default: - return ((Math.floor(now.getTime() / periodMs) * periodMs) / - 1000) as Time; - } - }; - - const updateRealTimeCandle = () => { - if (!seriesRef.current) return; - - const currentCandleStartTime = getCurrentCandleStartTime(); - - if ( - !lastCandleRef.current || - lastCandleRef.current.time !== currentCandleStartTime - ) { - const newCandle = { - time: currentCandleStartTime, - open: currentPrice, - high: currentPrice, - low: currentPrice, - close: currentPrice, - }; - lastCandleRef.current = newCandle; - seriesRef.current.update(newCandle); - } else { - const updatedCandle = { - ...lastCandleRef.current, - close: currentPrice, - high: Math.max(lastCandleRef.current.high, currentPrice), - low: Math.min(lastCandleRef.current.low, currentPrice), - }; - lastCandleRef.current = updatedCandle; - seriesRef.current.update(updatedCandle); - } - }; - - useEffect(() => { - if (!chartRef.current) return; - chartInstanceRef.current = initializeChart(chartRef.current, chartConfig); - seriesRef.current = setupCandlestickSeries( - chartInstanceRef.current, - [], - chartConfig, - ); - const resizeObserver = new ResizeObserver(() => { - handleResize(chartRef, chartInstanceRef); - }); - - if (chartRef.current.parentElement) { - resizeObserver.observe(chartRef.current.parentElement); - } - - return () => { - if (chartInstanceRef.current) { - resizeObserver.disconnect(); - chartInstanceRef.current.remove(); - } - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, []); + const { chartRef, chartInstanceRef, seriesRef } = useChartSetup(); + const { lastCandleRef } = useRealTimeCandle({ + seriesRef, + currentPrice, + activePeriod, + refetch, + minute, + }); useEffect(() => { if (!seriesRef.current || !chartInstanceRef.current) return; @@ -158,24 +50,6 @@ function CandleChart({ .subscribeVisibleLogicalRangeChange(handleScroll(fetchNextPage)); }, []); - useEffect(() => { - if (!seriesRef.current) return; - - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - - updateRealTimeCandle(); - - const updateInterval = activePeriod === 'minutes' ? 1000 : 5000; - intervalRef.current = setInterval(updateRealTimeCandle, updateInterval); - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [currentPrice, minute]); return
; } From 38de64927b83ab16cb366dbfab4ddb6f2861629d Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:56:21 +0900 Subject: [PATCH 54/59] =?UTF-8?q?feat:=20=EC=B0=A8=ED=8A=B8=20=EC=8B=A4?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80?= =?UTF-8?q?=EB=8F=99=20=ED=9B=84=20refetch=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/pages/trade/components/chart/Chart.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/client/src/pages/trade/components/chart/Chart.tsx b/packages/client/src/pages/trade/components/chart/Chart.tsx index 77ce831b..f62011ae 100644 --- a/packages/client/src/pages/trade/components/chart/Chart.tsx +++ b/packages/client/src/pages/trade/components/chart/Chart.tsx @@ -1,4 +1,4 @@ -import { usePeriodChart } from '@/hooks/market/usePeriodChart'; +import { usePeriodChart } from '@/hooks/chart/usePeriodChart'; import { useState } from 'react'; import ChartSelector from '@/pages/trade/components/chart/ChartSelector'; import { CandlePeriod } from '@/types/chart'; @@ -12,7 +12,11 @@ type ChartProps = { function Chart({ market, currentPrice }: ChartProps) { const [activePeriod, setActivePeriod] = useState('days'); const [minute, setMinute] = useState(); - const { data, fetchNextPage } = usePeriodChart(market, activePeriod, minute); + const { data, refetch, fetchNextPage } = usePeriodChart( + market, + activePeriod, + minute, + ); const handleActivePeriod = (period: CandlePeriod, minute?: number) => { setActivePeriod(period); @@ -29,6 +33,7 @@ function Chart({ market, currentPrice }: ChartProps) { activePeriod={activePeriod} minute={minute} data={data.candles.flat()} + refetch={refetch} fetchNextPage={fetchNextPage} currentPrice={currentPrice} /> From 487a563d25c7e4c152f3204d34cc43560068cf52 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:56:32 +0900 Subject: [PATCH 55/59] =?UTF-8?q?chore:=20utlitiy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/trade/components/order_form/OrderSellForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx index bdbc771d..a357218f 100644 --- a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx +++ b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx @@ -7,7 +7,7 @@ import { useCheckCoin } from '@/hooks/trade/useCheckCoin'; import { useMarketParams } from '@/hooks/market/useMarketParams'; import { useOrderForm } from '@/hooks/trade/useOrderForm'; import { useMyAccount } from '@/hooks/auth/useMyAccount'; -import { calculateProfitInfo } from '@/utility/calculateProfit'; +import { calculateProfitInfo } from '@/utility/finance/calculateProfit'; type OrderSellFormProps = { currentPrice: number; From 17a8ca12d2050f8df6298cd211eae4f388d5638e Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:56:43 +0900 Subject: [PATCH 56/59] =?UTF-8?q?chore:=20utlitiy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/trade/components/order_form/common/OrderSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/pages/trade/components/order_form/common/OrderSummary.tsx b/packages/client/src/pages/trade/components/order_form/common/OrderSummary.tsx index d0921b55..17f1dbcb 100644 --- a/packages/client/src/pages/trade/components/order_form/common/OrderSummary.tsx +++ b/packages/client/src/pages/trade/components/order_form/common/OrderSummary.tsx @@ -1,4 +1,4 @@ -import { calculateTotalPrice } from '@/utility/order'; +import { calculateTotalPrice } from '@/utility/finance/calculateTotalPrice'; interface OrderSummaryProps { price: string; From 0ffe1b7a077b57c4addfd458ebdbc91e3950596d Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 15:56:50 +0900 Subject: [PATCH 57/59] =?UTF-8?q?chore:=20utlitiy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/store/authStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/store/authStore.ts b/packages/client/src/store/authStore.ts index 592034c4..cc3d87b7 100644 --- a/packages/client/src/store/authStore.ts +++ b/packages/client/src/store/authStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import { getCookie, removeCookie } from '@/utility/cookies'; +import { getCookie, removeCookie } from '@/utility/storage/cookies'; interface AuthState { isAuthenticated: boolean; isLoading: boolean; From 637ead414520e840c62157a11931e5778750e9b1 Mon Sep 17 00:00:00 2001 From: qwer0114 Date: Tue, 26 Nov 2024 16:03:52 +0900 Subject: [PATCH 58/59] =?UTF-8?q?refactor:=20=EC=86=8C=EC=88=98=EC=A0=90?= =?UTF-8?q?=20=EA=B0=80=EA=B2=A9=20=EA=B5=AC=EB=A7=A4=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=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 --- .../src/pages/trade/components/order_form/OrderBuyForm.tsx | 2 +- .../src/pages/trade/components/order_form/OrderSellForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx index 841f3487..01854de6 100644 --- a/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx +++ b/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx @@ -36,7 +36,7 @@ function OrderBuyForm({ currentPrice, selectPrice }: OrderBuyFormProsp) {
Date: Tue, 26 Nov 2024 18:00:37 +0900 Subject: [PATCH 59/59] =?UTF-8?q?refactor:=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/components/Header.tsx | 2 +- .../client/src/components/sidebar/Sidebar.tsx | 4 +-- packages/client/src/pages/layout/Layout.tsx | 15 ++++++---- packages/client/src/pages/trade/Trade.tsx | 28 ++++++++++--------- .../pages/trade/components/chart/Chart.tsx | 2 +- .../trade/components/order_book/OrderBook.tsx | 2 +- .../trade/components/order_form/OrderForm.tsx | 2 +- .../components/trade_footer/TradeFooter.tsx | 4 +-- .../components/trade_header/TradeHeader.tsx | 2 +- .../client/src/utility/chart/chartEvent.ts | 4 ++- 10 files changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/client/src/components/Header.tsx b/packages/client/src/components/Header.tsx index 6a56c5d9..55e16b68 100644 --- a/packages/client/src/components/Header.tsx +++ b/packages/client/src/components/Header.tsx @@ -22,7 +22,7 @@ function Header() { <>
diff --git a/packages/client/src/components/sidebar/Sidebar.tsx b/packages/client/src/components/sidebar/Sidebar.tsx index 2ea0b963..fd12a9d9 100644 --- a/packages/client/src/components/sidebar/Sidebar.tsx +++ b/packages/client/src/components/sidebar/Sidebar.tsx @@ -45,9 +45,9 @@ function Sidebar() { ]; return ( -
+
-
+
{SIDEBAR_BUTTONS.map((button) => ( -
-
-
+
+
+
+
-
+ +
+ +
- +
); } diff --git a/packages/client/src/pages/trade/Trade.tsx b/packages/client/src/pages/trade/Trade.tsx index 915dfc57..6e0126f1 100644 --- a/packages/client/src/pages/trade/Trade.tsx +++ b/packages/client/src/pages/trade/Trade.tsx @@ -36,23 +36,25 @@ function Trade() { const currentPrice = price[market]?.trade_price; return ( -
- -
- }> - - - - + <> +
+ +
+ }> + + + + +
-
+ ); } diff --git a/packages/client/src/pages/trade/components/chart/Chart.tsx b/packages/client/src/pages/trade/components/chart/Chart.tsx index f62011ae..d1120313 100644 --- a/packages/client/src/pages/trade/components/chart/Chart.tsx +++ b/packages/client/src/pages/trade/components/chart/Chart.tsx @@ -24,7 +24,7 @@ function Chart({ market, currentPrice }: ChartProps) { }; return ( -
+
+
호가
diff --git a/packages/client/src/pages/trade/components/order_form/OrderForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderForm.tsx index 7702f220..48412914 100644 --- a/packages/client/src/pages/trade/components/order_form/OrderForm.tsx +++ b/packages/client/src/pages/trade/components/order_form/OrderForm.tsx @@ -17,7 +17,7 @@ function OrderForm({ currentPrice, selectPrice }: OrderFormProps) { const TABS = createOrderTabs({ currentPrice, selectPrice }); return ( -
+
주문하기
{TABS.map((tab) => ( diff --git a/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx b/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx index 06d03f37..22db83fc 100644 --- a/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx +++ b/packages/client/src/pages/trade/components/trade_footer/TradeFooter.tsx @@ -9,9 +9,9 @@ function TradeFooter() { const duplicatedCoins = coins ? [...coins, ...coins, ...coins, ...coins] : []; return ( -
+
-
+
{duplicatedCoins?.map((coin, index) => ( +
{ if (chartRef.current && chartInstanceRef.current) { const { width } = - chartRef.current.parentElement?.getBoundingClientRect() || { width: 0 }; + chartRef.current.parentElement?.getBoundingClientRect() || { + width: 0, + }; chartInstanceRef.current.applyOptions({ width: width, });