From 57bc13d6e836f9fcff396aa15cc3b77ed5ba9997 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 25 Nov 2024 12:13:48 +0900 Subject: [PATCH 001/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20=EC=B9=B4?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EB=8F=99=EC=9D=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/strategy/kakao.strategy.ts | 2 -- BE/src/auth/user.repository.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/BE/src/auth/strategy/kakao.strategy.ts b/BE/src/auth/strategy/kakao.strategy.ts index 1e4e0c85..bc392a76 100644 --- a/BE/src/auth/strategy/kakao.strategy.ts +++ b/BE/src/auth/strategy/kakao.strategy.ts @@ -48,9 +48,7 @@ export class KakaoStrategy extends PassportStrategy( // eslint-disable-next-line no-underscore-dangle const kakaoId = profile._json.id; // eslint-disable-next-line no-underscore-dangle - const { email } = profile._json.kakao_account; const user = { - email, kakaoId, }; done(null, user); diff --git a/BE/src/auth/user.repository.ts b/BE/src/auth/user.repository.ts index c17ce75f..4259adc1 100644 --- a/BE/src/auth/user.repository.ts +++ b/BE/src/auth/user.repository.ts @@ -43,10 +43,14 @@ export class UserRepository extends Repository { await queryRunner.startTransaction(); try { - const { kakaoId, email } = authCredentialsDto; + const { kakaoId } = authCredentialsDto; const salt: string = await bcrypt.genSalt(); const hashedPassword: string = await bcrypt.hash(String(kakaoId), salt); - const user = this.create({ email, kakaoId, password: hashedPassword }); + const user = this.create({ + email: hashedPassword, + kakaoId, + password: hashedPassword, + }); await this.save(user); const asset = this.assetRepository.create({ user_id: user.id }); await queryRunner.manager.save(asset); From 682742074071aab56d6dbaeb379581ba01fe3652 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 13:02:53 +0900 Subject: [PATCH 002/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9B=B9=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#180?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 13 +++++ BE/src/asset/asset.module.ts | 7 ++- BE/src/asset/asset.service.ts | 52 ++++++++++++++----- .../asset/dto/stock-element-response.dto.ts | 27 +++++++++- BE/src/asset/user-stock.repository.ts | 11 ++-- .../history/stock-trade-history.module.ts | 1 + 6 files changed, 93 insertions(+), 18 deletions(-) diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index 360b85f4..50c90ca8 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -71,6 +71,19 @@ export class AssetController { return this.assetService.getMyPage(parseInt(request.user.userId, 10)); } + @Get('/unsubscribe') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '마이페이지 내 주식 소켓 연결 취소 API', + description: '마이페이지에서 나갈 때 소켓 연결 취소를 위한 API', + }) + async unsubscribeMyStocks(@Req() request: Request) { + await this.assetService.unsubscribeMyStocks( + parseInt(request.user.userId, 10), + ); + } + @Cron('*/10 9-16 * * 1-5') async updateAllAssets() { await this.assetService.updateAllAssets(); diff --git a/BE/src/asset/asset.module.ts b/BE/src/asset/asset.module.ts index 0b582ee1..db1dc8ad 100644 --- a/BE/src/asset/asset.module.ts +++ b/BE/src/asset/asset.module.ts @@ -7,9 +7,14 @@ import { Asset } from './asset.entity'; import { UserStock } from './user-stock.entity'; import { UserStockRepository } from './user-stock.repository'; import { StockDetailModule } from '../stock/detail/stock-detail.module'; +import { StockTradeHistoryModule } from '../stock/trade/history/stock-trade-history.module'; @Module({ - imports: [TypeOrmModule.forFeature([Asset, UserStock]), StockDetailModule], + imports: [ + TypeOrmModule.forFeature([Asset, UserStock]), + StockDetailModule, + StockTradeHistoryModule, + ], controllers: [AssetController], providers: [AssetService, AssetRepository, UserStockRepository], exports: [AssetRepository, UserStockRepository], diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index cf6c04db..a4e00b25 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -7,6 +7,8 @@ import { AssetResponseDto } from './dto/asset-response.dto'; import { StockDetailService } from '../stock/detail/stock-detail.service'; import { UserStock } from './user-stock.entity'; import { Asset } from './asset.entity'; +import { InquirePriceResponseDto } from '../stock/detail/dto/stock-detail-response.dto'; +import { StockTradeHistorySocketService } from '../stock/trade/history/stock-trade-history-socket.service'; @Injectable() export class AssetService { @@ -14,6 +16,7 @@ export class AssetService { private readonly userStockRepository: UserStockRepository, private readonly assetRepository: AssetRepository, private readonly stockDetailService: StockDetailService, + private readonly stockTradeHistorySocketService: StockTradeHistorySocketService, ) {} async getUserStockByCode(userId: number, stockCode: string) { @@ -35,17 +38,21 @@ export class AssetService { const userStocks = await this.userStockRepository.findUserStockWithNameByUserId(userId); const asset = await this.assetRepository.findOneBy({ user_id: userId }); - const newAsset = await this.updateMyAsset( - asset, - await this.getCurrPrices(), - ); + const currPrices = await this.getCurrPrices(userId); + const newAsset = await this.updateMyAsset(asset, currPrices); const myStocks = userStocks.map((userStock) => { + const currPrice: InquirePriceResponseDto = + currPrices[userStock.stocks_code]; return new StockElementResponseDto( userStock.stocks_name, userStock.stocks_code, userStock.user_stocks_quantity, - userStock.user_stocks_avg_price, + Number(userStock.user_stocks_avg_price), + currPrice.stck_prpr, + currPrice.prdy_vrss, + currPrice.prdy_vrss_sign, + currPrice.prdy_ctrt, ); }); @@ -62,6 +69,8 @@ export class AssetService { response.asset = myAsset; response.stocks = myStocks; + await this.subscribeMyStocks(userId); + return response; } @@ -82,7 +91,8 @@ export class AssetService { const totalPrice = userStocks.reduce( (sum, userStock) => - sum + userStock.quantity * currPrices[userStock.stock_code], + sum + + userStock.quantity * Number(currPrices[userStock.stock_code].stck_prpr), 0, ); @@ -98,20 +108,38 @@ export class AssetService { return this.assetRepository.save(updatedAsset); } - private async getCurrPrices() { + private async getCurrPrices(userId?: number) { const userStocks: UserStock[] = - await this.userStockRepository.findAllDistinctCode(); + await this.userStockRepository.findAllDistinctCode(userId); const currPrices = {}; await Promise.allSettled( userStocks.map(async (userStock) => { - const inquirePrice = await this.stockDetailService.getInquirePrice( - userStock.stock_code, - ); - currPrices[userStock.stock_code] = Number(inquirePrice.stck_prpr); + currPrices[userStock.stock_code] = + await this.stockDetailService.getInquirePrice(userStock.stock_code); }), ); return currPrices; } + + private async subscribeMyStocks(userId: number) { + const userStocks: UserStock[] = + await this.userStockRepository.findAllDistinctCode(userId); + + userStocks.map((userStock) => + this.stockTradeHistorySocketService.subscribeByCode(userStock.stock_code), + ); + } + + async unsubscribeMyStocks(userId: number) { + const userStocks: UserStock[] = + await this.userStockRepository.findAllDistinctCode(userId); + + userStocks.map((userStock) => + this.stockTradeHistorySocketService.unsubscribeByCode( + userStock.stock_code, + ), + ); + } } diff --git a/BE/src/asset/dto/stock-element-response.dto.ts b/BE/src/asset/dto/stock-element-response.dto.ts index d2ce6d27..686ffee1 100644 --- a/BE/src/asset/dto/stock-element-response.dto.ts +++ b/BE/src/asset/dto/stock-element-response.dto.ts @@ -1,11 +1,24 @@ import { ApiProperty } from '@nestjs/swagger'; export class StockElementResponseDto { - constructor(name, code, quantity, avg_price) { + constructor( + name: string, + code: string, + quantity: number, + avg_price: number, + stck_prpr: string, + prdy_vrss: string, + prdy_vrss_sign: string, + prdy_ctrt: string, + ) { this.name = name; this.code = code; this.quantity = quantity; this.avg_price = avg_price; + this.stck_prpr = stck_prpr; + this.prdy_vrss = prdy_vrss; + this.prdy_vrss_sign = prdy_vrss_sign; + this.prdy_ctrt = prdy_ctrt; } @ApiProperty({ description: '종목 이름' }) @@ -19,4 +32,16 @@ export class StockElementResponseDto { @ApiProperty({ description: '평균 매수가' }) avg_price: number; + + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; } diff --git a/BE/src/asset/user-stock.repository.ts b/BE/src/asset/user-stock.repository.ts index 6542047f..656d6568 100644 --- a/BE/src/asset/user-stock.repository.ts +++ b/BE/src/asset/user-stock.repository.ts @@ -21,10 +21,13 @@ export class UserStockRepository extends Repository { .getRawMany(); } - findAllDistinctCode() { - return this.createQueryBuilder('user_stocks') + findAllDistinctCode(userId?: number) { + const queryBuilder = this.createQueryBuilder('user_stocks') .select('DISTINCT user_stocks.stock_code') - .where({ quantity: MoreThan(0) }) - .getRawMany(); + .where({ quantity: MoreThan(0) }); + + if (userId) queryBuilder.andWhere({ user_id: userId }); + + return queryBuilder.getRawMany(); } } diff --git a/BE/src/stock/trade/history/stock-trade-history.module.ts b/BE/src/stock/trade/history/stock-trade-history.module.ts index ca5ce277..bcf067be 100644 --- a/BE/src/stock/trade/history/stock-trade-history.module.ts +++ b/BE/src/stock/trade/history/stock-trade-history.module.ts @@ -9,5 +9,6 @@ import { SocketModule } from '../../../common/websocket/socket.module'; imports: [KoreaInvestmentModule, SocketModule], controllers: [StockTradeHistoryController], providers: [StockTradeHistoryService, StockTradeHistorySocketService], + exports: [StockTradeHistorySocketService], }) export class StockTradeHistoryModule {} From bc2ec06e6ffe280b424020dedd517a78d56d2bcc Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 25 Nov 2024 14:08:08 +0900 Subject: [PATCH 003/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=89=B4=EC=8A=A4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=EC=9A=A9=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=80=EB=B9=84#181?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 ++ BE/src/news/news.controller.ts | 4 ++++ BE/src/news/news.module.ts | 9 +++++++++ BE/src/news/news.service.ts | 4 ++++ 4 files changed, 19 insertions(+) create mode 100644 BE/src/news/news.controller.ts create mode 100644 BE/src/news/news.module.ts create mode 100644 BE/src/news/news.service.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 40ed1831..91561ccc 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -18,6 +18,7 @@ import { StockTradeHistoryModule } from './stock/trade/history/stock-trade-histo import { RedisModule } from './common/redis/redis.module'; import { HTTPExceptionFilter } from './common/filters/http-exception.filter'; import { RankingModule } from './ranking/ranking.module'; +import { NewsModule } from './news/news.module'; @Module({ imports: [ @@ -35,6 +36,7 @@ import { RankingModule } from './ranking/ranking.module'; StockTradeHistoryModule, RedisModule, RankingModule, + NewsModule, ], controllers: [AppController], providers: [ diff --git a/BE/src/news/news.controller.ts b/BE/src/news/news.controller.ts new file mode 100644 index 00000000..4f05b7c3 --- /dev/null +++ b/BE/src/news/news.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('/api/news') +export class NewsController {} diff --git a/BE/src/news/news.module.ts b/BE/src/news/news.module.ts new file mode 100644 index 00000000..eac04847 --- /dev/null +++ b/BE/src/news/news.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { NewsController } from './news.controller'; +import { NewsService } from './news.service'; + +@Module({ + controllers: [NewsController], + providers: [NewsService], +}) +export class NewsModule {} diff --git a/BE/src/news/news.service.ts b/BE/src/news/news.service.ts new file mode 100644 index 00000000..d075fee6 --- /dev/null +++ b/BE/src/news/news.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class NewsService {} From 882276cf9c6ee19ee9f8d9ebe249dd048d927d82 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 15:45:01 +0900 Subject: [PATCH 004/128] =?UTF-8?q?=E2=9C=A8=20feat:=20socket=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/StocksDetail/PriceSection.tsx | 26 +++++++++++++++++++ FE/src/components/TopFive/TopFive.tsx | 1 + 2 files changed, 27 insertions(+) diff --git a/FE/src/components/StocksDetail/PriceSection.tsx b/FE/src/components/StocksDetail/PriceSection.tsx index 7e04ba8d..5b523215 100644 --- a/FE/src/components/StocksDetail/PriceSection.tsx +++ b/FE/src/components/StocksDetail/PriceSection.tsx @@ -34,6 +34,32 @@ export default function PriceSection() { [id, buttonFlag], ); + if (isLoading) return; + + // useEffect(() => { + // // 이벤트 리스너 등록 + // const handleTradeHistory = (chartData: PriceDataType) => { + // addData(chartData); + // }; + // + // // 소켓 이벤트 구독 + // socket.on(`trade-history/${id}`, handleTradeHistory); + // + // return () => { + // console.log('socket unSub!'); + // socket.off(`trade-history/${id}`, handleTradeHistory); + // + // fetch(`/api/stocks/trade-history/${id}/unsubscribe`, { + // method: 'GET', + // headers: { + // 'Content-Type': 'application/json', + // }, + // }).catch((error) => { + // console.error('Failed to unsubscribe:', error); + // }); + // }; + // }, [id, addData]); + useEffect(() => { if (!buttonFlag) return; const eventSource = createSSEConnection( diff --git a/FE/src/components/TopFive/TopFive.tsx b/FE/src/components/TopFive/TopFive.tsx index a56dec60..aef005b1 100644 --- a/FE/src/components/TopFive/TopFive.tsx +++ b/FE/src/components/TopFive/TopFive.tsx @@ -19,6 +19,7 @@ export default function TopFive() { queryKey: ['topfive', currentMarket], queryFn: () => getTopFiveStocks(paramsMap[currentMarket]), keepPreviousData: true, + staleTime: 1000, cacheTime: 30000, // refetchInterval: 1000, }); From 89d8677ad568d3504a17d9452d4a370bdba167d5 Mon Sep 17 00:00:00 2001 From: dongree Date: Mon, 25 Nov 2024 15:57:11 +0900 Subject: [PATCH 005/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=98=84=EC=9E=AC?= =?UTF-8?q?=20=EC=9E=90=EC=82=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20?= =?UTF-8?q?api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StocksDetail/BuySection.tsx | 23 +++++++++++++------ .../StocksDetail/TradeAlertModal.tsx | 6 +++-- .../components/StocksDetail/TradeSection.tsx | 2 +- FE/src/service/assets.ts | 16 +++++++++++++ 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/FE/src/components/StocksDetail/BuySection.tsx b/FE/src/components/StocksDetail/BuySection.tsx index 79cab033..6ea73383 100644 --- a/FE/src/components/StocksDetail/BuySection.tsx +++ b/FE/src/components/StocksDetail/BuySection.tsx @@ -4,15 +4,20 @@ import { StockDetailType } from 'types'; import { isNumericString } from 'utils/common'; import TradeAlertModal from './TradeAlertModal'; import useAuthStore from 'store/authStore'; -const MyAsset = 10000000; +import { useQuery } from '@tanstack/react-query'; +import { getCash } from 'service/assets'; type BuySectionProps = { code: string; - data: StockDetailType; + detailInfo: StockDetailType; }; -export default function BuySection({ code, data }: BuySectionProps) { - const { stck_prpr, stck_mxpr, stck_llam } = data; +export default function BuySection({ code, detailInfo }: BuySectionProps) { + const { stck_prpr, stck_mxpr, stck_llam, hts_kor_isnm } = detailInfo; + + const { data, isLoading, isError } = useQuery(['detail', 'cash'], () => + getCash(), + ); const [currPrice, setCurrPrice] = useState(stck_prpr); const { isLogin } = useAuthStore(); @@ -32,6 +37,10 @@ export default function BuySection({ code, data }: BuySectionProps) { setCurrPrice(e.target.value); }; + if (isLoading) return
loading
; + if (!data) return
No data
; + if (isError) return
error
; + const handlePriceInputBlur = (e: FocusEvent) => { const n = +e.target.value; if (n > +stck_mxpr) { @@ -66,7 +75,7 @@ export default function BuySection({ code, data }: BuySectionProps) { const price = +currPrice * count; - if (price > MyAsset) { + if (price > data.cash_balance) { setLackAssetFlag(true); timerRef.current = window.setTimeout(() => { setLackAssetFlag(false); @@ -117,7 +126,7 @@ export default function BuySection({ code, data }: BuySectionProps) {

매수 가능 금액

-

0원

+

{data.cash_balance.toLocaleString()}원

총 주문 금액

@@ -142,7 +151,7 @@ export default function BuySection({ code, data }: BuySectionProps) { {isOpen && ( diff --git a/FE/src/components/StocksDetail/TradeAlertModal.tsx b/FE/src/components/StocksDetail/TradeAlertModal.tsx index d3fcc272..5c25393e 100644 --- a/FE/src/components/StocksDetail/TradeAlertModal.tsx +++ b/FE/src/components/StocksDetail/TradeAlertModal.tsx @@ -24,6 +24,8 @@ export default function TradeAlertModal({ if (res.ok) toggleModal(); }; + const totalPrice = +price * count; + return ( <> toggleModal()} /> @@ -34,7 +36,7 @@ export default function TradeAlertModal({

{count}주 희망가격

-

{(+price).toLocaleString()}원

+

{totalPrice.toLocaleString()}원

예상 수수료

@@ -42,7 +44,7 @@ export default function TradeAlertModal({

총 주문 금액

-

{(+price + charge).toLocaleString()}원

+

{(totalPrice + charge).toLocaleString()}원

diff --git a/FE/src/components/StocksDetail/TradeSection.tsx b/FE/src/components/StocksDetail/TradeSection.tsx index d30188c9..2f18bd21 100644 --- a/FE/src/components/StocksDetail/TradeSection.tsx +++ b/FE/src/components/StocksDetail/TradeSection.tsx @@ -59,7 +59,7 @@ export default function TradeSection({ code, data }: TradeSectionProps) {
{category === 'buy' ? ( - + ) : ( )} diff --git a/FE/src/service/assets.ts b/FE/src/service/assets.ts index a88249d2..7340df77 100644 --- a/FE/src/service/assets.ts +++ b/FE/src/service/assets.ts @@ -12,3 +12,19 @@ export async function getAssets(): Promise { }, }).then((res) => res.json()); } + +export async function getCash(): Promise<{ cash_balance: number }> { + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/assets/cash` + : '/api/assets/cash'; + + return fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => { + if (res.status === 401) return { cash_balance: 0 }; + return res.json(); + }); +} From 4add332573ea8d20d36845ca11748c702f3ff0d9 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 16:09:46 +0900 Subject: [PATCH 006/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20useQuery=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StockIndex/index.tsx | 15 ++++++++------- FE/src/components/StocksDetail/PriceSection.tsx | 2 -- FE/src/service/getStockIndex.ts | 7 +++++++ 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 FE/src/service/getStockIndex.ts diff --git a/FE/src/components/StockIndex/index.tsx b/FE/src/components/StockIndex/index.tsx index 133d04c7..6b0adb1e 100644 --- a/FE/src/components/StockIndex/index.tsx +++ b/FE/src/components/StockIndex/index.tsx @@ -1,21 +1,22 @@ import { Card } from './Card.tsx'; import { useQuery } from '@tanstack/react-query'; +import { getStockIndex } from '../../service/getStockIndex.ts'; export default function StockIndex() { - const { data, isLoading } = useQuery({ + const { data, isLoading, isError } = useQuery({ queryKey: ['StockIndex'], - queryFn: () => - fetch(`${import.meta.env.VITE_API_URL}/stocks/index`).then((res) => - res.json(), - ), + queryFn: () => getStockIndex(), + staleTime: 1000, + cacheTime: 60000, }); - if (isLoading) return; + if (isLoading) return
Loading...
; + if (isError) return
Error loading data
; const { KOSPI, KOSDAQ, KOSPI200, KSQ150 } = data; return ( -
+
diff --git a/FE/src/components/StocksDetail/PriceSection.tsx b/FE/src/components/StocksDetail/PriceSection.tsx index 5b523215..dd8436a2 100644 --- a/FE/src/components/StocksDetail/PriceSection.tsx +++ b/FE/src/components/StocksDetail/PriceSection.tsx @@ -34,8 +34,6 @@ export default function PriceSection() { [id, buttonFlag], ); - if (isLoading) return; - // useEffect(() => { // // 이벤트 리스너 등록 // const handleTradeHistory = (chartData: PriceDataType) => { diff --git a/FE/src/service/getStockIndex.ts b/FE/src/service/getStockIndex.ts new file mode 100644 index 00000000..e5f23d48 --- /dev/null +++ b/FE/src/service/getStockIndex.ts @@ -0,0 +1,7 @@ +export const getStockIndex = async () => { + const response = await fetch(`${import.meta.env.VITE_API_URL}/stocks/index`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); +}; From 937da869ad6742a7bfbe1d9153a86223a547e000 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 16:28:49 +0900 Subject: [PATCH 007/128] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=B2=84=ED=8A=BC=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=B5=9C=EC=A0=81=ED=99=94.=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EB=B2=84=ED=8A=BC=EC=9D=84=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=20=ED=95=B4=EB=8B=B9=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EC=97=90=20=EC=9E=88=EC=96=B4=EB=8F=84=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EA=B0=80=20=EB=8B=A4=EC=8B=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81?= =?UTF-8?q?=EC=9D=B4=20=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=98=EC=97=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/Header.tsx | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/FE/src/components/Header.tsx b/FE/src/components/Header.tsx index b027c866..3381cb00 100644 --- a/FE/src/components/Header.tsx +++ b/FE/src/components/Header.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import useAuthStore from 'store/authStore'; import useLoginModalStore from 'store/useLoginModalStore'; import useSearchModalStore from '../store/useSearchModalStore.ts'; @@ -12,6 +12,8 @@ export default function Header() { const { isLogin, setIsLogin } = useAuthStore(); const { toggleSearchModal } = useSearchModalStore(); const { searchInput } = useSearchInputStore(); + const location = useLocation(); + const navigate = useNavigate(); useEffect(() => { const check = async () => { @@ -29,6 +31,13 @@ export default function Header() { }); }; + const handleLink = (to: string) => { + if (location.pathname === to) { + return; + } + navigate(to); + }; + return (
@@ -39,9 +48,26 @@ export default function Header() {
Date: Mon, 25 Nov 2024 16:32:54 +0900 Subject: [PATCH 008/128] =?UTF-8?q?=E2=9C=A8=20feat:=20sellSection=20layou?= =?UTF-8?q?t=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/StocksDetail/SellSection.tsx | 185 +++++++++++++++++- .../components/StocksDetail/TradeSection.tsx | 2 +- FE/src/service/assets.ts | 18 ++ 3 files changed, 195 insertions(+), 10 deletions(-) diff --git a/FE/src/components/StocksDetail/SellSection.tsx b/FE/src/components/StocksDetail/SellSection.tsx index b34136ee..dd5bd294 100644 --- a/FE/src/components/StocksDetail/SellSection.tsx +++ b/FE/src/components/StocksDetail/SellSection.tsx @@ -1,15 +1,182 @@ import Lottie from 'lottie-react'; import emptyAnimation from 'assets/emptyAnimation.json'; +import { useQuery } from '@tanstack/react-query'; +import { getSellPossibleStockCnt } from 'service/assets'; +import { ChangeEvent, FocusEvent, FormEvent, useRef, useState } from 'react'; +import { StockDetailType } from 'types'; +import useAuthStore from 'store/authStore'; +import useTradeAlertModalStore from 'store/tradeAlertModalStore'; +import { isNumericString } from 'utils/common'; + +type SellSectionProps = { + code: string; + detailInfo: StockDetailType; +}; + +export default function SellSection({ code, detailInfo }: SellSectionProps) { + const { stck_prpr, stck_mxpr, stck_llam, hts_kor_isnm } = detailInfo; + + const { data, isLoading, isError } = useQuery( + ['detail', 'sellPosiible', code], + () => getSellPossibleStockCnt(code), + ); + + const [currPrice, setCurrPrice] = useState(stck_prpr); + const { isLogin } = useAuthStore(); + const [count, setCount] = useState(0); + + const [upperLimitFlag, setUpperLimitFlag] = useState(false); + const [lowerLimitFlag, setLowerLimitFlag] = useState(false); + const [cntFlag, setCntFlag] = useState(false); + const timerRef = useRef(null); + + const { isOpen, toggleModal } = useTradeAlertModalStore(); + + if (isLoading) return
loading
; + if (!data) return
No data
; + if (isError) return
error
; + + const quantity = data.quantity; + const handlePriceChange = (e: ChangeEvent) => { + if (!isNumericString(e.target.value)) return; + + setCurrPrice(e.target.value); + }; + + const handlePriceInputBlur = (e: FocusEvent) => { + const n = +e.target.value; + if (n > +stck_mxpr) { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + setCurrPrice(stck_mxpr); + + setUpperLimitFlag(true); + timerRef.current = window.setTimeout(() => { + setUpperLimitFlag(false); + }, 2000); + return; + } + + if (n < +stck_llam) { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + setCurrPrice(stck_llam); + + setLowerLimitFlag(true); + timerRef.current = window.setTimeout(() => { + setLowerLimitFlag(false); + }, 2000); + return; + } + }; + + const handleCntInputBlur = (e: FocusEvent) => { + const n = +e.target.value; + if (n > quantity) { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + setCount(quantity); + + setCntFlag(true); + timerRef.current = window.setTimeout(() => { + setCntFlag(false); + }, 2000); + } + }; + + const handleSell = async (e: FormEvent) => { + e.preventDefault(); + + // const price = +currPrice * count; + + // if (price > data.cash_balance) { + // setLackAssetFlag(true); + // timerRef.current = window.setTimeout(() => { + // setLackAssetFlag(false); + // }, 2000); + // return; + // } + toggleModal(); + }; + + if (!isLogin || quantity === 0) { + return ( +
+ +

매도할 주식이 없어요

+
+ ); + } -export default function SellSection() { return ( -
- -

매도할 주식이 없어요

-
+ <> +
+
+
+

매도 가격

+ +
+ {lowerLimitFlag && ( +
+ 이 주식의 최소 가격은 {(+stck_llam).toLocaleString()}입니다. +
+ )} + {upperLimitFlag && ( +
+ 이 주식의 최대 가격은 {(+stck_mxpr).toLocaleString()}입니다. +
+ )} +
+

수량

+ setCount(+e.target.value)} + onBlur={handleCntInputBlur} + className='flex-1 py-1 rounded-lg' + min={1} + max={quantity} + /> +
+ {cntFlag && ( +
+ {quantity}주 까지 판매할 수 있어요. +
+ )} +
+ +
+ +
+
+

총 매도 금액

+

{(+currPrice * count).toLocaleString()}원

+
+
+ +
+ +
+ ); } diff --git a/FE/src/components/StocksDetail/TradeSection.tsx b/FE/src/components/StocksDetail/TradeSection.tsx index 2f18bd21..7e5f688d 100644 --- a/FE/src/components/StocksDetail/TradeSection.tsx +++ b/FE/src/components/StocksDetail/TradeSection.tsx @@ -61,7 +61,7 @@ export default function TradeSection({ code, data }: TradeSectionProps) { {category === 'buy' ? ( ) : ( - + )} diff --git a/FE/src/service/assets.ts b/FE/src/service/assets.ts index 7340df77..98596288 100644 --- a/FE/src/service/assets.ts +++ b/FE/src/service/assets.ts @@ -28,3 +28,21 @@ export async function getCash(): Promise<{ cash_balance: number }> { return res.json(); }); } + +export async function getSellPossibleStockCnt( + code: string, +): Promise<{ quantity: number }> { + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/assets/stocks/${code}` + : `/api/assets/stocks/${code}`; + + return fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => { + if (res.status === 401) return { quantity: 0 }; + return res.json(); + }); +} From 899cfbb6a7beff836ff10b42b0d101f647e79796 Mon Sep 17 00:00:00 2001 From: dongree Date: Mon, 25 Nov 2024 16:43:13 +0900 Subject: [PATCH 009/128] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20trade?= =?UTF-8?q?=20section=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 --- .../{ => TradeSection}/BuySection.tsx | 2 +- .../{ => TradeSection}/SellSection.tsx | 0 .../{ => TradeSection}/TradeAlertModal.tsx | 0 .../index.tsx} | 2 +- FE/src/components/StocksDetail/dummy.ts | 611 ------------------ 5 files changed, 2 insertions(+), 613 deletions(-) rename FE/src/components/StocksDetail/{ => TradeSection}/BuySection.tsx (100%) rename FE/src/components/StocksDetail/{ => TradeSection}/SellSection.tsx (100%) rename FE/src/components/StocksDetail/{ => TradeSection}/TradeAlertModal.tsx (100%) rename FE/src/components/StocksDetail/{TradeSection.tsx => TradeSection/index.tsx} (100%) delete mode 100644 FE/src/components/StocksDetail/dummy.ts diff --git a/FE/src/components/StocksDetail/BuySection.tsx b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx similarity index 100% rename from FE/src/components/StocksDetail/BuySection.tsx rename to FE/src/components/StocksDetail/TradeSection/BuySection.tsx index 6ea73383..1dc2f49a 100644 --- a/FE/src/components/StocksDetail/BuySection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx @@ -2,10 +2,10 @@ import { ChangeEvent, FocusEvent, FormEvent, useRef, useState } from 'react'; import useTradeAlertModalStore from 'store/tradeAlertModalStore'; import { StockDetailType } from 'types'; import { isNumericString } from 'utils/common'; -import TradeAlertModal from './TradeAlertModal'; import useAuthStore from 'store/authStore'; import { useQuery } from '@tanstack/react-query'; import { getCash } from 'service/assets'; +import TradeAlertModal from './TradeAlertModal'; type BuySectionProps = { code: string; diff --git a/FE/src/components/StocksDetail/SellSection.tsx b/FE/src/components/StocksDetail/TradeSection/SellSection.tsx similarity index 100% rename from FE/src/components/StocksDetail/SellSection.tsx rename to FE/src/components/StocksDetail/TradeSection/SellSection.tsx diff --git a/FE/src/components/StocksDetail/TradeAlertModal.tsx b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx similarity index 100% rename from FE/src/components/StocksDetail/TradeAlertModal.tsx rename to FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx diff --git a/FE/src/components/StocksDetail/TradeSection.tsx b/FE/src/components/StocksDetail/TradeSection/index.tsx similarity index 100% rename from FE/src/components/StocksDetail/TradeSection.tsx rename to FE/src/components/StocksDetail/TradeSection/index.tsx index 7e5f688d..ae0bd351 100644 --- a/FE/src/components/StocksDetail/TradeSection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { StockDetailType } from 'types'; -import SellSection from './SellSection'; import BuySection from './BuySection'; +import SellSection from './SellSection'; type TradeSectionProps = { code: string; diff --git a/FE/src/components/StocksDetail/dummy.ts b/FE/src/components/StocksDetail/dummy.ts deleted file mode 100644 index ec2288da..00000000 --- a/FE/src/components/StocksDetail/dummy.ts +++ /dev/null @@ -1,611 +0,0 @@ -export type DummyStock = { - date: string; - open: number; - close: number; - high: number; - low: number; - volume: number; -}; - -export const dummy: DummyStock[] = [ - { - date: '2024-11-07', - open: 58000, - close: 57000, - high: 58300, - low: 57000, - volume: 13451844, - }, - { - date: '2024-11-06', - open: 56900, - close: 57500, - high: 58100, - low: 56800, - volume: 16951844, - }, - { - date: '2024-11-05', - open: 57600, - close: 57300, - high: 58000, - low: 56300, - volume: 21901844, - }, - { - date: '2024-11-04', - open: 57800, - close: 57600, - high: 58100, - low: 57200, - volume: 17291844, - }, - { - date: '2024-11-03', - open: 58600, - close: 58700, - high: 59400, - low: 58400, - volume: 15471844, - }, - { - date: '2024-10-31', - open: 59000, - close: 58300, - high: 59600, - low: 58100, - volume: 18831844, - }, - { - date: '2024-10-30', - open: 58500, - close: 59200, - high: 61200, - low: 58300, - volume: 34901844, - }, - { - date: '2024-10-29', - open: 59100, - close: 59100, - high: 59800, - low: 58600, - volume: 19181844, - }, - { - date: '2024-10-28', - open: 58000, - close: 59600, - high: 59600, - low: 57300, - volume: 27871844, - }, - { - date: '2024-10-27', - open: 55700, - close: 58100, - high: 58500, - low: 55700, - volume: 27571844, - }, - { - date: '2024-10-24', - open: 56000, - close: 55900, - high: 56900, - low: 55800, - volume: 25511844, - }, - { - date: '2024-10-23', - open: 58200, - close: 56600, - high: 58500, - low: 56600, - volume: 30701844, - }, - { - date: '2024-10-22', - open: 57500, - close: 59100, - high: 60000, - low: 57100, - volume: 27111844, - }, - { - date: '2024-10-21', - open: 58800, - close: 57700, - high: 58900, - low: 57700, - volume: 26881844, - }, - { - date: '2024-10-20', - open: 59000, - close: 59000, - high: 59600, - low: 58500, - volume: 18331844, - }, - { - date: '2024-10-17', - open: 59900, - close: 59200, - high: 60100, - low: 59100, - volume: 14171844, - }, - { - date: '2024-10-16', - open: 59400, - close: 59700, - high: 60100, - low: 59100, - volume: 23001844, - }, - { - date: '2024-10-15', - open: 59400, - close: 59500, - high: 60000, - low: 59200, - volume: 23021844, - }, - { - date: '2024-10-14', - open: 61100, - close: 61000, - high: 61400, - low: 60100, - volume: 20651844, - }, - { - date: '2024-10-13', - open: 59500, - close: 60800, - high: 61200, - low: 59400, - volume: 20371844, - }, - { - date: '2024-10-10', - open: 59100, - close: 59300, - high: 60100, - low: 59000, - volume: 28901844, - }, - { - date: '2024-10-09', - open: 60100, - close: 58900, - high: 60200, - low: 58900, - volume: 44601844, - }, - { - date: '2024-10-08', - open: 60000, - close: 60100, - high: 60500, - low: 59800, - volume: 18631844, - }, - { - date: '2024-10-07', - open: 59800, - close: 60000, - high: 60300, - low: 59500, - volume: 17651844, - }, - { - date: '2024-10-06', - open: 59500, - close: 59800, - high: 60000, - low: 59300, - volume: 16671844, - }, - { - date: '2024-10-03', - open: 59000, - close: 59200, - high: 59500, - low: 58800, - volume: 16671844, - }, - { - date: '2024-10-02', - open: 59500, - close: 59000, - high: 59800, - low: 58800, - volume: 17651844, - }, - { - date: '2024-10-01', - open: 60000, - close: 59500, - high: 60200, - low: 59300, - volume: 18631844, - }, - { - date: '2024-09-30', - open: 60500, - close: 60000, - high: 60800, - low: 59800, - volume: 19611844, - }, - { - date: '2024-09-27', - open: 61000, - close: 60500, - high: 61200, - low: 60300, - volume: 20591844, - }, - { - date: '2024-09-26', - open: 61500, - close: 61000, - high: 61800, - low: 60800, - volume: 21571844, - }, - { - date: '2024-09-25', - open: 62000, - close: 61500, - high: 62300, - low: 61300, - volume: 22551844, - }, - { - date: '2024-09-24', - open: 62500, - close: 62000, - high: 62800, - low: 61800, - volume: 23531844, - }, - { - date: '2024-09-23', - open: 63000, - close: 62500, - high: 63300, - low: 62300, - volume: 24511844, - }, - { - date: '2024-09-20', - open: 63500, - close: 63000, - high: 63800, - low: 62800, - volume: 25491844, - }, - { - date: '2024-09-19', - open: 64000, - close: 63500, - high: 64300, - low: 63300, - volume: 26471844, - }, - { - date: '2024-09-18', - open: 64500, - close: 64000, - high: 64800, - low: 63800, - volume: 27451844, - }, - { - date: '2024-09-17', - open: 65000, - close: 64500, - high: 65300, - low: 64300, - volume: 28431844, - }, - { - date: '2024-09-16', - open: 65500, - close: 65000, - high: 65800, - low: 64800, - volume: 29411844, - }, - { - date: '2024-09-13', - open: 66000, - close: 65500, - high: 66300, - low: 65300, - volume: 30391844, - }, - { - date: '2024-09-12', - open: 66500, - close: 66000, - high: 66800, - low: 65800, - volume: 31371844, - }, - { - date: '2024-09-11', - open: 67000, - close: 66500, - high: 67300, - low: 66300, - volume: 32351844, - }, - { - date: '2024-09-10', - open: 67500, - close: 67000, - high: 67800, - low: 66800, - volume: 33331844, - }, - { - date: '2024-09-09', - open: 68000, - close: 67500, - high: 68300, - low: 67300, - volume: 34311844, - }, - { - date: '2024-09-06', - open: 68500, - close: 68000, - high: 68800, - low: 67800, - volume: 35291844, - }, - { - date: '2024-09-05', - open: 69000, - close: 68500, - high: 69300, - low: 68300, - volume: 36271844, - }, - { - date: '2024-09-04', - open: 69500, - close: 69000, - high: 69800, - low: 68800, - volume: 37251844, - }, - { - date: '2024-09-03', - open: 70000, - close: 69500, - high: 70300, - low: 69300, - volume: 38231844, - }, - { - date: '2024-09-02', - open: 70500, - close: 70000, - high: 70800, - low: 69800, - volume: 39211844, - }, - { - date: '2024-08-30', - open: 71000, - close: 70500, - high: 71300, - low: 70300, - volume: 40191844, - }, - { - date: '2024-08-29', - open: 71500, - close: 71000, - high: 71800, - low: 70800, - volume: 40191844, - }, - { - date: '2024-08-28', - open: 72000, - close: 71500, - high: 72300, - low: 71300, - volume: 39171844, - }, - { - date: '2024-08-27', - open: 72500, - close: 72000, - high: 72800, - low: 71800, - volume: 38151844, - }, - { - date: '2024-08-26', - open: 73000, - close: 72500, - high: 73300, - low: 72300, - volume: 37131844, - }, - { - date: '2024-08-23', - open: 73500, - close: 73000, - high: 73800, - low: 72800, - volume: 36111844, - }, - { - date: '2024-08-22', - open: 74000, - close: 73500, - high: 74300, - low: 73300, - volume: 35091844, - }, - { - date: '2024-08-21', - open: 74500, - close: 74000, - high: 74800, - low: 73800, - volume: 34071844, - }, - { - date: '2024-08-20', - open: 75000, - close: 74500, - high: 75300, - low: 74300, - volume: 33051844, - }, - { - date: '2024-08-19', - open: 75500, - close: 75000, - high: 75800, - low: 74800, - volume: 32031844, - }, - { - date: '2024-08-16', - open: 76000, - close: 75500, - high: 76300, - low: 75300, - volume: 31011844, - }, - { - date: '2024-08-15', - open: 76500, - close: 76000, - high: 76800, - low: 75800, - volume: 29991844, - }, - { - date: '2024-08-14', - open: 77000, - close: 76500, - high: 77300, - low: 76300, - volume: 28971844, - }, - { - date: '2024-08-13', - open: 77500, - close: 77000, - high: 77800, - low: 76800, - volume: 27951844, - }, - { - date: '2024-08-12', - open: 78000, - close: 77500, - high: 78300, - low: 77300, - volume: 26931844, - }, - { - date: '2024-08-09', - open: 78500, - close: 78000, - high: 78800, - low: 77800, - volume: 25911844, - }, - { - date: '2024-08-08', - open: 79000, - close: 78500, - high: 79300, - low: 78300, - volume: 24891844, - }, - { - date: '2024-08-07', - open: 79500, - close: 79000, - high: 79800, - low: 78800, - volume: 23871844, - }, - { - date: '2024-08-06', - open: 80000, - close: 79500, - high: 80300, - low: 79300, - volume: 22851844, - }, - { - date: '2024-08-05', - open: 80500, - close: 80000, - high: 80800, - low: 79800, - volume: 21831844, - }, - { - date: '2024-08-02', - open: 81000, - close: 80500, - high: 81300, - low: 80300, - volume: 20811844, - }, - { - date: '2024-08-01', - open: 81500, - close: 81000, - high: 81800, - low: 80800, - volume: 19791844, - }, - { - date: '2024-07-31', - open: 82000, - close: 81500, - high: 82300, - low: 81300, - volume: 18771844, - }, - { - date: '2024-07-30', - open: 82500, - close: 82000, - high: 82800, - low: 81800, - volume: 17751844, - }, - { - date: '2024-07-29', - open: 83000, - close: 82500, - high: 83300, - low: 82300, - volume: 16731844, - }, - { - date: '2024-07-26', - open: 83500, - close: 83000, - high: 83800, - low: 82800, - volume: 15711844, - }, -].reverse(); From c7ddaec013b17f50d95b074c5b78a69731c82a5f Mon Sep 17 00:00:00 2001 From: dongree Date: Mon, 25 Nov 2024 16:50:58 +0900 Subject: [PATCH 010/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A7=A4=EB=8F=84?= =?UTF-8?q?=20modal=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StocksDetail/TradeSection/BuySection.tsx | 1 + .../StocksDetail/TradeSection/SellSection.tsx | 10 ++++++++++ .../TradeSection/TradeAlertModal.tsx | 20 ++++++++++++------- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/FE/src/components/StocksDetail/TradeSection/BuySection.tsx b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx index 1dc2f49a..b4994854 100644 --- a/FE/src/components/StocksDetail/TradeSection/BuySection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/BuySection.tsx @@ -154,6 +154,7 @@ export default function BuySection({ code, detailInfo }: BuySectionProps) { stockName={hts_kor_isnm} price={currPrice} count={count} + type='BUY' /> )} diff --git a/FE/src/components/StocksDetail/TradeSection/SellSection.tsx b/FE/src/components/StocksDetail/TradeSection/SellSection.tsx index dd5bd294..eeb8f1cf 100644 --- a/FE/src/components/StocksDetail/TradeSection/SellSection.tsx +++ b/FE/src/components/StocksDetail/TradeSection/SellSection.tsx @@ -7,6 +7,7 @@ import { StockDetailType } from 'types'; import useAuthStore from 'store/authStore'; import useTradeAlertModalStore from 'store/tradeAlertModalStore'; import { isNumericString } from 'utils/common'; +import TradeAlertModal from './TradeAlertModal'; type SellSectionProps = { code: string; @@ -177,6 +178,15 @@ export default function SellSection({ code, detailInfo }: SellSectionProps) { 매도하기 + {isOpen && ( + + )} ); } diff --git a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx index 5c25393e..b20b4803 100644 --- a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx +++ b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx @@ -7,6 +7,7 @@ type TradeAlertModalProps = { stockName: string; price: string; count: number; + type: 'SELL' | 'BUY'; }; export default function TradeAlertModal({ @@ -14,14 +15,19 @@ export default function TradeAlertModal({ stockName, price, count, + type, }: TradeAlertModalProps) { const { toggleModal } = useTradeAlertModalStore(); const charge = 55; // 수수료 임시 - const handleBuy = async () => { - const res = await orderBuyStock(code, +price, count); - if (res.ok) toggleModal(); + const handleTrade = async () => { + if (type === 'BUY') { + const res = await orderBuyStock(code, +price, count); + if (res.ok) toggleModal(); + } else { + // 매도 api + } }; const totalPrice = +price * count; @@ -31,7 +37,7 @@ export default function TradeAlertModal({ toggleModal()} />
- {stockName} 매수 {count}주 + {stockName} {type === 'BUY' ? '매수' : '매도'} {count}주
@@ -56,10 +62,10 @@ export default function TradeAlertModal({ 취소
From 75adac86d9118ed8b29f32c7bbaa957ba8a14d9c Mon Sep 17 00:00:00 2001 From: dongree Date: Mon, 25 Nov 2024 17:01:10 +0900 Subject: [PATCH 011/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A7=A4=EB=8F=84?= =?UTF-8?q?=20API=20=EC=97=B0=EB=8F=99=20#137?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TradeSection/TradeAlertModal.tsx | 5 ++-- FE/src/service/orders.ts | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx index b20b4803..029b99dd 100644 --- a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx +++ b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx @@ -1,5 +1,5 @@ import Overay from 'components/ModalOveray'; -import { orderBuyStock } from 'service/orders'; +import { orderBuyStock, orderSellStock } from 'service/orders'; import useTradeAlertModalStore from 'store/tradeAlertModalStore'; type TradeAlertModalProps = { @@ -26,7 +26,8 @@ export default function TradeAlertModal({ const res = await orderBuyStock(code, +price, count); if (res.ok) toggleModal(); } else { - // 매도 api + const res = await orderSellStock(code, +price, count); + if (res.ok) toggleModal(); } }; diff --git a/FE/src/service/orders.ts b/FE/src/service/orders.ts index 381b225f..c19a5509 100644 --- a/FE/src/service/orders.ts +++ b/FE/src/service/orders.ts @@ -23,6 +23,29 @@ export async function orderBuyStock( }); } +export async function orderSellStock( + code: string, + price: number, + amount: number, +) { + const url = import.meta.env.PROD + ? `${import.meta.env.VITE_API_URL}/stocks/order/sell` + : '/api/stocks/order/sell'; + + return fetch(url, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + stock_code: code, + price, + amount, + }), + }); +} + export async function getOrders(): Promise { const url = import.meta.env.PROD ? `${import.meta.env.VITE_API_URL}/stocks/order/list` From cf57abe698ef127389b264ba904dbfbe3131df59 Mon Sep 17 00:00:00 2001 From: dongree Date: Mon, 25 Nov 2024 17:09:41 +0900 Subject: [PATCH 012/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=88=98=EC=88=98?= =?UTF-8?q?=EB=A3=8C=20=EA=B3=84=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StocksDetail/TradeSection/TradeAlertModal.tsx | 11 ++++++----- FE/src/utils/common.ts | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx index 029b99dd..015f236d 100644 --- a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx +++ b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx @@ -1,6 +1,7 @@ import Overay from 'components/ModalOveray'; import { orderBuyStock, orderSellStock } from 'service/orders'; import useTradeAlertModalStore from 'store/tradeAlertModalStore'; +import { getTradeCommision } from 'utils/common'; type TradeAlertModalProps = { code: string; @@ -19,7 +20,9 @@ export default function TradeAlertModal({ }: TradeAlertModalProps) { const { toggleModal } = useTradeAlertModalStore(); - const charge = 55; // 수수료 임시 + const totalPrice = +price * count; + + const tradeCommission = getTradeCommision(totalPrice); // 수수료 임시 const handleTrade = async () => { if (type === 'BUY') { @@ -31,8 +34,6 @@ export default function TradeAlertModal({ } }; - const totalPrice = +price * count; - return ( <> toggleModal()} /> @@ -47,11 +48,11 @@ export default function TradeAlertModal({

예상 수수료

-

{charge}원

+

{tradeCommission.toLocaleString()}원

총 주문 금액

-

{(totalPrice + charge).toLocaleString()}원

+

{(totalPrice + tradeCommission).toLocaleString()}원

diff --git a/FE/src/utils/common.ts b/FE/src/utils/common.ts index d5a100c7..3310fb33 100644 --- a/FE/src/utils/common.ts +++ b/FE/src/utils/common.ts @@ -26,3 +26,14 @@ export function parseTimestamp(timestamp: string) { export function isNumericString(str: string) { return str.length === 0 || /^[0-9]+$/.test(str); } + +export function getTradeCommision(price: number) { + let rate = 0; + if (price <= 10_000_000) rate = 0.0016; + else if (price <= 50_000_000) rate = 0.0014; + else if (price <= 100_000_000) rate = 0.0012; + else if (price <= 300_000_000) rate = 0.001; + else rate = 0.0008; + + return Math.floor(price * rate); +} From 1e9de6dba51bd945950172dbebe065a0c5fa5a94 Mon Sep 17 00:00:00 2001 From: dongree Date: Mon, 25 Nov 2024 17:19:03 +0900 Subject: [PATCH 013/128] =?UTF-8?q?=F0=9F=92=84=20design:=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20Order=20=ED=8C=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/Mypage/Order.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/FE/src/components/Mypage/Order.tsx b/FE/src/components/Mypage/Order.tsx index 5f337d36..b8a2bbb0 100644 --- a/FE/src/components/Mypage/Order.tsx +++ b/FE/src/components/Mypage/Order.tsx @@ -43,7 +43,13 @@ export default function Order() {

{stock_name}

{stock_code}

-

+

{trade_type === 'BUY' ? '매수' : '매도'}

{amount}

@@ -54,7 +60,7 @@ export default function Order() {

From f62d4ac86a6ae6f805a86022c30b207d8d99f806 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 17:24:41 +0900 Subject: [PATCH 014/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=89=B4=EC=8A=A4?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B8=B0=EC=B4=88=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20#188=20=EB=89=B4=EC=8A=A4=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EC=9E=91=EC=97=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/News/CardWithImage.tsx | 3 +++ FE/src/components/News/CardWithoutImage.tsx | 3 +++ FE/src/components/News/Header.tsx | 7 +++++++ FE/src/components/News/News.tsx | 22 +++++++++++++++++++++ FE/src/page/Home.tsx | 2 ++ 5 files changed, 37 insertions(+) create mode 100644 FE/src/components/News/CardWithImage.tsx create mode 100644 FE/src/components/News/CardWithoutImage.tsx create mode 100644 FE/src/components/News/Header.tsx create mode 100644 FE/src/components/News/News.tsx diff --git a/FE/src/components/News/CardWithImage.tsx b/FE/src/components/News/CardWithImage.tsx new file mode 100644 index 00000000..3295e803 --- /dev/null +++ b/FE/src/components/News/CardWithImage.tsx @@ -0,0 +1,3 @@ +export default function CardWithImage() { + return

image
; +} diff --git a/FE/src/components/News/CardWithoutImage.tsx b/FE/src/components/News/CardWithoutImage.tsx new file mode 100644 index 00000000..db9ea5d2 --- /dev/null +++ b/FE/src/components/News/CardWithoutImage.tsx @@ -0,0 +1,3 @@ +export default function CardWithoutImage() { + return
No image
; +} diff --git a/FE/src/components/News/Header.tsx b/FE/src/components/News/Header.tsx new file mode 100644 index 00000000..7d3c7f67 --- /dev/null +++ b/FE/src/components/News/Header.tsx @@ -0,0 +1,7 @@ +export default function Header() { + return ( +
+
주요 뉴스
+
+ ); +} diff --git a/FE/src/components/News/News.tsx b/FE/src/components/News/News.tsx new file mode 100644 index 00000000..bfeb9125 --- /dev/null +++ b/FE/src/components/News/News.tsx @@ -0,0 +1,22 @@ +import Header from './Header.tsx'; +import CardWithImage from './CardWithImage.tsx'; +import CardWithoutImage from './CardWithoutImage.tsx'; + +export default function News() { + return ( +
+
+
    + +
+ +
    + +
+
+ ); +} diff --git a/FE/src/page/Home.tsx b/FE/src/page/Home.tsx index 3ae5da96..938d05f7 100644 --- a/FE/src/page/Home.tsx +++ b/FE/src/page/Home.tsx @@ -1,11 +1,13 @@ import TopFive from 'components/TopFive/TopFive'; import StockIndex from 'components/StockIndex/index.tsx'; +import News from '../components/News/News.tsx'; export default function Home() { return ( <> + ); } From ee0fb528722e0cf60893e3f940684f0a6c3b9541 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 17:25:48 +0900 Subject: [PATCH 015/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B4=80=EB=A0=A8=20=EC=BA=90=EC=8B=B1=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80=20#191?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/Search/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FE/src/components/Search/index.tsx b/FE/src/components/Search/index.tsx index 0fc866c2..c271ef96 100644 --- a/FE/src/components/Search/index.tsx +++ b/FE/src/components/Search/index.tsx @@ -28,6 +28,8 @@ export default function SearchModal() { queryKey: ['search', debounceValue], queryFn: () => getSearchResults(debounceValue), enabled: !!debounceValue && !isDebouncing, + staleTime: 1000, + cacheTime: 1000 * 60, }); useEffect(() => { From c7d1c6d24614786cdac0903655f8fd1d6d6a6455 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 17:42:57 +0900 Subject: [PATCH 016/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=88=98?= =?UTF-8?q?=EC=88=98=EB=A3=8C=20=EB=82=B4=EB=A6=BC=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order-socket.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts index 60c6afa1..5b1b5d5a 100644 --- a/BE/src/stock/order/stock-order-socket.service.ts +++ b/BE/src/stock/order/stock-order-socket.service.ts @@ -107,13 +107,13 @@ export class StockOrderSocketService { } private calculateFee(totalPrice: number) { - if (totalPrice <= 10000000) return totalPrice * 0.16; + if (totalPrice <= 10000000) return Math.floor(totalPrice * 0.16); if (totalPrice > 10000000 && totalPrice <= 50000000) - return totalPrice * 0.14; + return Math.floor(totalPrice * 0.14); if (totalPrice > 50000000 && totalPrice <= 100000000) - return totalPrice * 0.12; + return Math.floor(totalPrice * 0.12); if (totalPrice > 100000000 && totalPrice <= 300000000) - return totalPrice * 0.1; - return totalPrice * 0.08; + return Math.floor(totalPrice * 0.1); + return Math.floor(totalPrice * 0.08); } } From 7d4d4e889547601d2f66f76f5c7ad0059dcdece5 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 17:44:18 +0900 Subject: [PATCH 017/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4?= =?UTF-8?q?=EB=8F=84=20=EA=B0=80=EB=8A=A5=20=EC=A3=BC=EC=8B=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90=20=EB=A7=A4?= =?UTF-8?q?=EC=88=98=20=ED=8F=89=EA=B7=A0=EA=B0=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 9 +++++---- BE/src/asset/asset.service.ts | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts index 50c90ca8..df72e33d 100644 --- a/BE/src/asset/asset.controller.ts +++ b/BE/src/asset/asset.controller.ts @@ -20,13 +20,14 @@ export class AssetController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @ApiOperation({ - summary: '매도 가능 주식 개수 조회 API', - description: '특정 주식 매도 시에 필요한 매도 가능한 주식 개수를 조회한다.', + summary: '매도 가능 주식 개수, 매수 평균가 조회 API', + description: + '특정 주식 매도 시에 필요한 매도 가능한 주식 개수와 매수 평균가를 조회한다.', }) @ApiResponse({ status: 200, - description: '매도 가능 주식 개수 조회 성공', - example: { quantity: 0 }, + description: '매도 가능 주식 개수 및 매수 평균가 조회 성공', + example: { quantity: 0, avg_price: 0 }, }) async getUserStockByCode( @Req() request: Request, diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index a4e00b25..74e1cd15 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -25,7 +25,10 @@ export class AssetService { stock_code: stockCode, }); - return { quantity: userStock ? userStock.quantity : 0 }; + return { + quantity: userStock ? userStock.quantity : 0, + avg_price: userStock ? userStock.avg_price : 0, + }; } async getCashBalance(userId: number) { From 06b2b20559c5e380679e296ba9316daa66e15c4d Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 18:27:29 +0900 Subject: [PATCH 018/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4?= =?UTF-8?q?=EC=88=98=20=EC=A3=BC=EB=AC=B8=20=EC=8B=9C=20pending=20order?= =?UTF-8?q?=EB=8F=84=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.repository.ts | 16 +++++++++++++++- BE/src/asset/asset.service.ts | 11 ++++++++++- BE/src/stock/order/stock-order.service.ts | 13 ++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/BE/src/asset/asset.repository.ts b/BE/src/asset/asset.repository.ts index 3aac066b..81c926b3 100644 --- a/BE/src/asset/asset.repository.ts +++ b/BE/src/asset/asset.repository.ts @@ -2,10 +2,13 @@ import { DataSource, Repository } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; import { Injectable } from '@nestjs/common'; import { Asset } from './asset.entity'; +import { Order } from '../stock/order/stock-order.entity'; +import { StatusType } from '../stock/order/enum/status-type'; +import { TradeType } from '../stock/order/enum/trade-type'; @Injectable() export class AssetRepository extends Repository { - constructor(@InjectDataSource() dataSource: DataSource) { + constructor(@InjectDataSource() private readonly dataSource: DataSource) { super(Asset, dataSource.createEntityManager()); } @@ -15,4 +18,15 @@ export class AssetRepository extends Repository { .select(['asset.* ', 'user.nickname as nickname']) .getRawMany(); } + + async findAllPendingOrders(userId: number) { + const queryRunner = this.dataSource.createQueryRunner(); + return queryRunner.manager.find(Order, { + where: { + user_id: userId, + status: StatusType.PENDING, + trade_type: TradeType.BUY, + }, + }); + } } diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 74e1cd15..9618416c 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -9,6 +9,9 @@ import { UserStock } from './user-stock.entity'; import { Asset } from './asset.entity'; import { InquirePriceResponseDto } from '../stock/detail/dto/stock-detail-response.dto'; import { StockTradeHistorySocketService } from '../stock/trade/history/stock-trade-history-socket.service'; +import { StatusType } from '../stock/order/enum/status-type'; +import { TradeType } from '../stock/order/enum/trade-type'; +import { Order } from '../stock/order/stock-order.entity'; @Injectable() export class AssetService { @@ -33,8 +36,14 @@ export class AssetService { async getCashBalance(userId: number) { const asset = await this.assetRepository.findOneBy({ user_id: userId }); + const pendingOrders = + await this.assetRepository.findAllPendingOrders(userId); + const totalPendingPrice = pendingOrders.reduce( + (sum, pendingOrder) => sum + pendingOrder.price * pendingOrder.amount, + 0, + ); - return { cash_balance: asset.cash_balance }; + return { cash_balance: asset.cash_balance - totalPendingPrice }; } async getMyPage(userId: number) { diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 81002143..92ad1702 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -26,10 +26,21 @@ export class StockOrderService { async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { const asset = await this.assetRepository.findOneBy({ user_id: userId }); + const pendingOrders = await this.stockOrderRepository.findBy({ + user_id: userId, + status: StatusType.PENDING, + trade_type: TradeType.BUY, + stock_code: stockOrderRequest.stock_code, + }); + const totalPendingPrice = pendingOrders.reduce( + (sum, pendingOrder) => sum + pendingOrder.price * pendingOrder.amount, + 0, + ); if ( asset && - asset.cash_balance < stockOrderRequest.amount * stockOrderRequest.price + asset.cash_balance < + stockOrderRequest.amount * stockOrderRequest.price + totalPendingPrice ) throw new BadRequestException('가용 자산이 충분하지 않습니다.'); From 8e8716435049afa8f80e2e0087a3c80029fcc335 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 18:42:36 +0900 Subject: [PATCH 019/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4?= =?UTF-8?q?=EB=8F=84=20=EC=A3=BC=EB=AC=B8=20=EC=8B=9C=20pending=20order?= =?UTF-8?q?=EB=8F=84=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.repository.ts | 4 ++-- BE/src/asset/asset.service.ts | 18 +++++++++++++----- BE/src/stock/order/stock-order.service.ts | 15 ++++++++++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/BE/src/asset/asset.repository.ts b/BE/src/asset/asset.repository.ts index 81c926b3..977efd32 100644 --- a/BE/src/asset/asset.repository.ts +++ b/BE/src/asset/asset.repository.ts @@ -19,13 +19,13 @@ export class AssetRepository extends Repository { .getRawMany(); } - async findAllPendingOrders(userId: number) { + async findAllPendingOrders(userId: number, tradeType: TradeType) { const queryRunner = this.dataSource.createQueryRunner(); return queryRunner.manager.find(Order, { where: { user_id: userId, status: StatusType.PENDING, - trade_type: TradeType.BUY, + trade_type: tradeType, }, }); } diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 9618416c..2471c29b 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -9,9 +9,7 @@ import { UserStock } from './user-stock.entity'; import { Asset } from './asset.entity'; import { InquirePriceResponseDto } from '../stock/detail/dto/stock-detail-response.dto'; import { StockTradeHistorySocketService } from '../stock/trade/history/stock-trade-history-socket.service'; -import { StatusType } from '../stock/order/enum/status-type'; import { TradeType } from '../stock/order/enum/trade-type'; -import { Order } from '../stock/order/stock-order.entity'; @Injectable() export class AssetService { @@ -27,17 +25,27 @@ export class AssetService { user_id: userId, stock_code: stockCode, }); + const pendingOrders = await this.assetRepository.findAllPendingOrders( + userId, + TradeType.SELL, + ); + const totalPendingCount = pendingOrders.reduce( + (sum, pendingOrder) => sum + pendingOrder.amount, + 0, + ); return { - quantity: userStock ? userStock.quantity : 0, + quantity: userStock ? userStock.quantity - totalPendingCount : 0, avg_price: userStock ? userStock.avg_price : 0, }; } async getCashBalance(userId: number) { const asset = await this.assetRepository.findOneBy({ user_id: userId }); - const pendingOrders = - await this.assetRepository.findAllPendingOrders(userId); + const pendingOrders = await this.assetRepository.findAllPendingOrders( + userId, + TradeType.BUY, + ); const totalPendingPrice = pendingOrders.reduce( (sum, pendingOrder) => sum + pendingOrder.price * pendingOrder.amount, 0, diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 92ad1702..bbc4572f 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -62,8 +62,21 @@ export class StockOrderService { user_id: userId, stock_code: stockOrderRequest.stock_code, }); + const pendingOrders = await this.stockOrderRepository.findBy({ + user_id: userId, + status: StatusType.PENDING, + trade_type: TradeType.SELL, + stock_code: stockOrderRequest.stock_code, + }); + const totalPendingCount = pendingOrders.reduce( + (sum, pendingOrder) => sum + pendingOrder.amount, + 0, + ); - if (!userStock || userStock.quantity < stockOrderRequest.amount) + if ( + !userStock || + userStock.quantity < stockOrderRequest.amount + totalPendingCount + ) throw new BadRequestException('주식을 매도 수만큼 가지고 있지 않습니다.'); const order = this.stockOrderRepository.create({ From 8aba9b65f74867027f0cd9d44a16c809ecfc166f Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 25 Nov 2024 19:01:48 +0900 Subject: [PATCH 020/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20ranking-data,?= =?UTF-8?q?=20ranking-result=20dto=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B4=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/dto/ranking-data.dto.ts | 12 ++++------ BE/src/ranking/dto/ranking-result.dto.ts | 6 ++--- BE/src/ranking/ranking.service.ts | 30 +++++++++++++++++------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/BE/src/ranking/dto/ranking-data.dto.ts b/BE/src/ranking/dto/ranking-data.dto.ts index 97094da9..5b015797 100644 --- a/BE/src/ranking/dto/ranking-data.dto.ts +++ b/BE/src/ranking/dto/ranking-data.dto.ts @@ -8,16 +8,14 @@ export class RankingDataDto { nickname: string; @ApiProperty({ - description: '수익률 (%)', - example: 15.7, - required: false, + description: '랭킹 순위', + example: 1, }) - profitRate?: number; + rank: number; @ApiProperty({ - description: '총 자산', - example: 1000000, + description: '수익률 (%) 혹은 총 자산', required: false, }) - totalAsset?: number; + value: number; } diff --git a/BE/src/ranking/dto/ranking-result.dto.ts b/BE/src/ranking/dto/ranking-result.dto.ts index 61fa5888..48790194 100644 --- a/BE/src/ranking/dto/ranking-result.dto.ts +++ b/BE/src/ranking/dto/ranking-result.dto.ts @@ -9,9 +9,9 @@ export class RankingResultDto { topRank: RankingDataDto[]; @ApiProperty({ - description: '현재 사용자의 순위 (없을 경우 null)', - example: 42, + description: '현재 사용자의 랭킹 데이터', + example: { rank: 1, value: 10000, nickname: 'trader' }, nullable: true, }) - userRank: number | null; + userRank: RankingDataDto; } diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index dc8e6b96..edf1d064 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -49,18 +49,19 @@ export class RankingService { sortBy === SortType.PROFIT_RATE ? JSON.stringify({ nickname: rank.nickname, - profitRate: rank.profitRate, + value: Math.trunc(rank.profitRate * 100) / 100, }) : JSON.stringify({ nickname: rank.nickname, - totalAsset: rank.totalAsset, + rank, + value: rank.totalAsset, }), ), ), ); } - const findUserRank = async () => { + const findUserRankWithValue = async () => { if (!options.nickname) return null; const members = await this.redisDomainService.zrange(key, 0, -1); @@ -69,24 +70,35 @@ export class RankingService { return parsed.nickname === options.nickname; }); + const parsedUserMember = JSON.parse(userMember); return userMember - ? this.redisDomainService.zrevrank(key, userMember) + ? { + nickname: parsedUserMember.nickname, + rank: (await this.redisDomainService.zrevrank(key, userMember)) + 1, + value: + sortBy === SortType.PROFIT_RATE + ? parsedUserMember.value + : parsedUserMember.value, + } : null; }; + const userRankWithValue = await findUserRankWithValue(); + const [topRank, userRank] = await Promise.all([ this.redisDomainService.zrevrange(key, 0, 9), - findUserRank(), + userRankWithValue, ]); - const parsedTopRank: RankingDataDto[] = topRank.map((rank) => + const parsedTopRank: RankingDataDto[] = topRank.map((rank) => { + const { nickname, value } = JSON.parse(rank); // eslint-disable-next-line @typescript-eslint/no-unsafe-return - JSON.parse(rank), - ); + return { nickname, rank: topRank.indexOf(rank) + 1, value }; + }); return { topRank: parsedTopRank, - userRank: userRank !== null ? userRank + 1 : null, + userRank: userRank, }; } From a0b8e0f1dddd4b36060f31db07d1028fba297b94 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 19:14:10 +0900 Subject: [PATCH 021/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=89=B4=EC=8A=A4?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?#188?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/News/CardWithImage.tsx | 33 ++++++++++++++- FE/src/components/News/CardWithoutImage.tsx | 11 ++++- FE/src/components/News/Header.tsx | 7 ---- FE/src/components/News/News.tsx | 21 ++++++---- FE/src/components/News/newsMockData.ts | 45 +++++++++++++++++++++ 5 files changed, 99 insertions(+), 18 deletions(-) delete mode 100644 FE/src/components/News/Header.tsx create mode 100644 FE/src/components/News/newsMockData.ts diff --git a/FE/src/components/News/CardWithImage.tsx b/FE/src/components/News/CardWithImage.tsx index 3295e803..ada1e378 100644 --- a/FE/src/components/News/CardWithImage.tsx +++ b/FE/src/components/News/CardWithImage.tsx @@ -1,3 +1,32 @@ -export default function CardWithImage() { - return
image
; +import { NewsMockDataType } from './newsMockData.ts'; + +type CardWithImageProps = { + data: NewsMockDataType; +}; +export default function CardWithImage({ data }: CardWithImageProps) { + return ( +
+ {/* 이미지 컨테이너 */} +
+ news thumbnail +
+ +
+ {/* 뉴스 제목 */} +

+ {data.title} +

+ + {/* 날짜와 출판사 */} +
+
{data.date}
+
{data.publisher}
+
+
+
+ ); } diff --git a/FE/src/components/News/CardWithoutImage.tsx b/FE/src/components/News/CardWithoutImage.tsx index db9ea5d2..8fa0501a 100644 --- a/FE/src/components/News/CardWithoutImage.tsx +++ b/FE/src/components/News/CardWithoutImage.tsx @@ -1,3 +1,12 @@ export default function CardWithoutImage() { - return
No image
; + return ( +
+ + [단독] 반도체 산업 신규 투자 확대...정부 지원책 발표 + + + 중앙일보 + +
+ ); } diff --git a/FE/src/components/News/Header.tsx b/FE/src/components/News/Header.tsx deleted file mode 100644 index 7d3c7f67..00000000 --- a/FE/src/components/News/Header.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Header() { - return ( -
-
주요 뉴스
-
- ); -} diff --git a/FE/src/components/News/News.tsx b/FE/src/components/News/News.tsx index bfeb9125..71b360a1 100644 --- a/FE/src/components/News/News.tsx +++ b/FE/src/components/News/News.tsx @@ -1,20 +1,25 @@ -import Header from './Header.tsx'; import CardWithImage from './CardWithImage.tsx'; import CardWithoutImage from './CardWithoutImage.tsx'; +import { newsMockData, NewsMockDataType } from './newsMockData.ts'; export default function News() { return (
-
-
    - +
    +
    주요 뉴스
    +
    +
      + {newsMockData.slice(0, 4).map((data: NewsMockDataType) => ( + + ))}
    -
      +
        + + +
diff --git a/FE/src/components/News/newsMockData.ts b/FE/src/components/News/newsMockData.ts new file mode 100644 index 00000000..8fbff356 --- /dev/null +++ b/FE/src/components/News/newsMockData.ts @@ -0,0 +1,45 @@ +export type NewsMockDataType = { + publisher: string; + img: string; + title: string; + date: string; + link: string; +}; + +export const newsMockData: NewsMockDataType[] = [ + { + publisher: '중앙일보', + img: 'https://s.pstatic.net/dthumb.phinf/?src=%22https%3A%2F%2Fs.pstatic.net%2Fstatic%2Fnewsstand%2F2024%2F1125%2Farticle_img%2Fnew_main%2F9152%2F071102_001.jpg%22&type=nf312_208&service=navermain', + title: '[단독] 반도체 산업 신규 투자 확대...정부 지원책 발표', + date: '11월 03일 18:55 직접 편집', + link: 'https://www.joongang.co.kr/article/25288962', + }, + { + publisher: '동아일보', + img: 'https://s.pstatic.net/dthumb.phinf/?src=%22https%3A%2F%2Fs.pstatic.net%2Fstatic%2Fnewsstand%2F2024%2F1125%2Farticle_img%2Fnew_main%2F9131%2F172557_001.jpg%22&type=nf312_208&service=navermain', + title: '청년 주거대책 발표...월세 지원 확대', + date: '11월 03일 18:39 직접 편집', + link: 'https://www.donga.com/news/article/all/20241103/123456', + }, + { + publisher: '한국경제', + img: 'https://s.pstatic.net/dthumb.phinf/?src=%22https%3A%2F%2Fs.pstatic.net%2Fstatic%2Fnewsstand%2F2024%2F1125%2Farticle_img%2Fnew_main%2F9029%2F175118_001.jpg%22&type=nf312_208&service=navermain', + title: '기준금리 동결 전망...시장 영향은?', + date: '11월 03일 17:50 직접 편집', + link: 'https://www.hankyung.com/article/2024110387654', + }, + { + publisher: '매일경제', + img: 'https://s.pstatic.net/dthumb.phinf/?src=%22https%3A%2F%2Fs.pstatic.net%2Fstatic%2Fnewsstand%2F2024%2F1125%2Farticle_img%2Fnew_main%2F9243%2F174551_001.jpg%22&type=nf312_208&service=navermain', + title: '글로벌 기업들의 韓 투자 러시...배경은?', + date: '11월 03일 17:30 직접 편집', + link: 'https://www.mk.co.kr/news/2024/11/123987', + }, + { + publisher: '한겨레', + img: 'https://s.pstatic.net/static/newsstand/2024/1103/article_img/9005/171525_001.jpg', + title: '환경부, 신재생에너지 정책 전면 개편 추진', + date: '11월 03일 17:15 직접 편집', + link: 'https://www.hani.co.kr/arti/20241103-654321', + }, +]; From bdbfbd06850f68054d67c1649b56b92f3e5da686 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 25 Nov 2024 19:36:10 +0900 Subject: [PATCH 022/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20BE=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=203=EA=B0=9C=EB=A5=BC=20=EA=B0=81=EA=B0=81=20deploy?= =?UTF-8?q?=20=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 --- .github/workflows/deploy-production.yml | 52 +++++++++++++++++++------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index dfd62551..799446c3 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -18,8 +18,34 @@ jobs: matrix: app: [ - { name: 'be', dir: 'BE', port: 3000, container: 'juga-docker-be' }, - { name: 'fe', dir: 'FE', port: 5173, container: 'juga-docker-fe' }, + { + name: 'be-1', + dir: 'BE', + port: 3000, + container: 'juga-docker-be-1', + env: 'ENV_BE_1', + }, + { + name: 'be-2', + dir: 'BE', + port: 3001, + container: 'juga-docker-be-2', + env: 'ENV_BE_2', + }, + { + name: 'be-3', + dir: 'BE', + port: 3002, + container: 'juga-docker-be-3', + env: 'ENV_BE_3', + }, + { + name: 'fe', + dir: 'FE', + port: 5173, + container: 'juga-docker-fe', + env: 'ENV_FE', + }, ] steps: @@ -35,7 +61,7 @@ jobs: - name: Create .env file run: | touch ./${{ matrix.app.dir }}/.env - echo "${{ secrets.ENV }}" > ./${{matrix.app.dir}}/.env + echo "${{ secrets[matrix.app.env] }}" > ./${{matrix.app.dir}}/.env - name: Install dependencies working-directory: ./${{matrix.app.dir}} @@ -43,7 +69,7 @@ jobs: run: npm ci - name: Run tests - if: ${{ matrix.app.name == 'be' }} + if: ${{ contains(matrix.app.name, 'be') }} working-directory: ./${{matrix.app.dir}} run: npm test env: @@ -116,15 +142,17 @@ jobs: script: | docker system prune -af - echo "${{ secrets.ENV }}" > .env + echo "${{ secrets[matrix.app.env] }}" > .env-${{ matrix.app.name }} docker network create juga-network || true - docker run -d \ - --name redis \ - --network juga-network \ - -p 6379:6379 \ - -v redis_data:/data \ - redis:latest redis-server --appendonly yes + if [ "${{ matrix.app.name }}" = "be-1" ]; then + docker run -d \ + --name redis \ + --network juga-network \ + -p 6379:6379 \ + -v redis_data:/data \ + redis:latest redis-server --appendonly yes + fi docker pull ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} docker stop ${{ matrix.app.container }} || true @@ -133,7 +161,7 @@ jobs: --name ${{ matrix.app.container }} \ --network juga-network \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ - --env-file .env \ + --env-file .env-${{matrix.app.name}} \ ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} - name: Remove Github Action Ip to Security group From 14925488a5e0ed22e53b2bf111ae3964c6ba35d7 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 19:36:14 +0900 Subject: [PATCH 023/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EA=B4=80=EB=A0=A8=20fetch=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=97=90=20=EC=BF=A0=ED=82=A4=20=ED=8F=AC=ED=95=A8=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/service/getRanking.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FE/src/service/getRanking.ts b/FE/src/service/getRanking.ts index 2c5221d9..a966695b 100644 --- a/FE/src/service/getRanking.ts +++ b/FE/src/service/getRanking.ts @@ -1,5 +1,8 @@ export const getRanking = async () => { - const response = await fetch(`${import.meta.env.VITE_API_URL}/ranking`); + const response = await fetch(`${import.meta.env.VITE_API_URL}/ranking`, { + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + }); if (!response.ok) { throw new Error('Network response was not ok'); } From 483768c0aab877a97921e725b058134052684837 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 19:40:56 +0900 Subject: [PATCH 024/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EA=B8=B0=EB=B3=B8=20=ED=8B=80=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20#177=20#178=20#179?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/bookmark/stock-bookmark.controller.ts | 9 +++++++++ BE/src/stock/bookmark/stock-bookmark.entity.ts | 13 +++++++++++++ BE/src/stock/bookmark/stock-bookmark.module.ts | 13 +++++++++++++ BE/src/stock/bookmark/stock-bookmark.repository.ts | 11 +++++++++++ BE/src/stock/bookmark/stock-bookmark.service.ts | 9 +++++++++ 5 files changed, 55 insertions(+) create mode 100644 BE/src/stock/bookmark/stock-bookmark.controller.ts create mode 100644 BE/src/stock/bookmark/stock-bookmark.entity.ts create mode 100644 BE/src/stock/bookmark/stock-bookmark.module.ts create mode 100644 BE/src/stock/bookmark/stock-bookmark.repository.ts create mode 100644 BE/src/stock/bookmark/stock-bookmark.service.ts diff --git a/BE/src/stock/bookmark/stock-bookmark.controller.ts b/BE/src/stock/bookmark/stock-bookmark.controller.ts new file mode 100644 index 00000000..20a441e9 --- /dev/null +++ b/BE/src/stock/bookmark/stock-bookmark.controller.ts @@ -0,0 +1,9 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { StockBookmarkService } from './stock-bookmark.service'; + +@Controller('/api/stocks/bookmark') +@ApiTags('주식 즐겨찾기 API') +export class StockBookmarkController { + constructor(private readonly stockBookmarkService: StockBookmarkService) {} +} diff --git a/BE/src/stock/bookmark/stock-bookmark.entity.ts b/BE/src/stock/bookmark/stock-bookmark.entity.ts new file mode 100644 index 00000000..107a9572 --- /dev/null +++ b/BE/src/stock/bookmark/stock-bookmark.entity.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('bookmarks') +export class Bookmark { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: false }) + stock_code: string; + + @Column({ nullable: false }) + user_id: number; +} diff --git a/BE/src/stock/bookmark/stock-bookmark.module.ts b/BE/src/stock/bookmark/stock-bookmark.module.ts new file mode 100644 index 00000000..4296610a --- /dev/null +++ b/BE/src/stock/bookmark/stock-bookmark.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Bookmark } from './stock-bookmark.entity'; +import { StockBookmarkController } from './stock-bookmark.controller'; +import { StockBookmarkRepository } from './stock-bookmark.repository'; +import { StockBookmarkService } from './stock-bookmark.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Bookmark])], + controllers: [StockBookmarkController], + providers: [StockBookmarkRepository, StockBookmarkService], +}) +export class StockBookmarkModule {} diff --git a/BE/src/stock/bookmark/stock-bookmark.repository.ts b/BE/src/stock/bookmark/stock-bookmark.repository.ts new file mode 100644 index 00000000..c008180e --- /dev/null +++ b/BE/src/stock/bookmark/stock-bookmark.repository.ts @@ -0,0 +1,11 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { Bookmark } from './stock-bookmark.entity'; + +@Injectable() +export class StockBookmarkRepository extends Repository { + constructor(@InjectDataSource() private readonly dataSource: DataSource) { + super(Bookmark, dataSource.createEntityManager()); + } +} diff --git a/BE/src/stock/bookmark/stock-bookmark.service.ts b/BE/src/stock/bookmark/stock-bookmark.service.ts new file mode 100644 index 00000000..5e07cf85 --- /dev/null +++ b/BE/src/stock/bookmark/stock-bookmark.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { StockBookmarkRepository } from './stock-bookmark.repository'; + +@Injectable() +export class StockBookmarkService { + constructor( + private readonly stockBookmarkRepository: StockBookmarkRepository, + ) {} +} From 6fd7db9094adc58a346baeb5838804d513610cdd Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 25 Nov 2024 19:56:23 +0900 Subject: [PATCH 025/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=98=EB=8A=94=20env=20=ED=8C=8C=EC=9D=BC=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20env=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 799446c3..604c9be9 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -142,7 +142,7 @@ jobs: script: | docker system prune -af - echo "${{ secrets[matrix.app.env] }}" > .env-${{ matrix.app.name }} + echo "${{ secrets[matrix.app.env] }}" > ${{matrix.app.name}}.env docker network create juga-network || true if [ "${{ matrix.app.name }}" = "be-1" ]; then @@ -161,9 +161,11 @@ jobs: --name ${{ matrix.app.container }} \ --network juga-network \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ - --env-file .env-${{matrix.app.name}} \ + --env-file ${{matrix.app.name}}.env \ ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + rm ${{matrix.app.name}}.env + - name: Remove Github Action Ip to Security group run: | chmod -R 777 ~/cli_linux From 0c53b04baf80cbeb93cd9ee2100c317a8e088be1 Mon Sep 17 00:00:00 2001 From: Seo San Date: Mon, 25 Nov 2024 19:57:01 +0900 Subject: [PATCH 026/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=9E=AD=ED=82=B9?= =?UTF-8?q?=20API=20=EC=97=B0=EB=8F=99=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/Rank/RankCard.tsx | 27 ++---- FE/src/components/Rank/RankList.tsx | 9 +- FE/src/components/Rank/RankType.ts | 13 ++- FE/src/components/Rank/bummyData.ts | 133 ---------------------------- FE/src/page/Rank.tsx | 17 ++-- 5 files changed, 34 insertions(+), 165 deletions(-) delete mode 100644 FE/src/components/Rank/bummyData.ts diff --git a/FE/src/components/Rank/RankCard.tsx b/FE/src/components/Rank/RankCard.tsx index 3e74496f..fef5a8e8 100644 --- a/FE/src/components/Rank/RankCard.tsx +++ b/FE/src/components/Rank/RankCard.tsx @@ -1,18 +1,11 @@ -import { AssetRankItemType, ProfitRankItemType } from './bummyData.ts'; +import { RankingItem } from './RankType.ts'; type Props = { - item: ProfitRankItemType | AssetRankItemType; - ranking: number; + item: RankingItem; type: '수익률순' | '자산순'; }; -export default function RankCard({ item, ranking, type }: Props) { - const isProfitRankItem = ( - item: ProfitRankItemType | AssetRankItemType, - ): item is ProfitRankItemType => { - return 'profitRate' in item; - }; - +export default function RankCard({ item, type }: Props) { return (
- {ranking + 1} + {item.rank}
{item.nickname}
{type === '수익률순' - ? isProfitRankItem(item) - ? `${item.profitRate}%` - : '0%' + ? `${item.value}%` : new Intl.NumberFormat('ko-KR', { notation: 'compact', maximumFractionDigits: 1, - }).format((item as AssetRankItemType).totalAsset) + '원'} + }).format((item as RankingItem).value) + '원'}
diff --git a/FE/src/components/Rank/RankList.tsx b/FE/src/components/Rank/RankList.tsx index 9bf814f5..13fdbcb9 100644 --- a/FE/src/components/Rank/RankList.tsx +++ b/FE/src/components/Rank/RankList.tsx @@ -1,10 +1,10 @@ import RankCard from './RankCard'; import useAuthStore from '../../store/authStore.ts'; -import { AssetRankingType, ProfitRankingType } from './bummyData.ts'; +import { RankingCategory } from './RankType.ts'; type Props = { title: '수익률순' | '자산순'; - data: ProfitRankingType | AssetRankingType; + data: RankingCategory; }; export default function RankList({ title, data }: Props) { @@ -22,20 +22,19 @@ export default function RankList({ title, data }: Props) { ))}
- {isLogin && userRank !== null && typeof userRank.rank === 'number' ? ( + {isLogin && userRank !== null ? (

{`내 ${title} 순위`}

- +
) : null} diff --git a/FE/src/components/Rank/RankType.ts b/FE/src/components/Rank/RankType.ts index bba826fa..1a4937db 100644 --- a/FE/src/components/Rank/RankType.ts +++ b/FE/src/components/Rank/RankType.ts @@ -1,4 +1,15 @@ -export type TmpDataType = { +export type RankingItem = { nickname: string; + rank: number; value: number; }; + +export type RankingCategory = { + topRank: RankingItem[]; + userRank: RankingItem; +}; + +export type RankingData = { + profitRateRanking: RankingCategory; + assetRanking: RankingCategory; +}; diff --git a/FE/src/components/Rank/bummyData.ts b/FE/src/components/Rank/bummyData.ts deleted file mode 100644 index acf27717..00000000 --- a/FE/src/components/Rank/bummyData.ts +++ /dev/null @@ -1,133 +0,0 @@ -// 타입 수정 -// 수익률 랭킹 타입 -export type ProfitRankItemType = { - nickname: string; - profitRate: number; - rank?: number; -}; - -// 수익률 랭킹 타입 -export type ProfitRankingType = { - topRank: ProfitRankItemType[]; - userRank: ProfitRankItemType; -}; - -// 자산 랭킹 타입 -export type AssetRankItemType = { - nickname: string; - totalAsset: number; - rank?: number; -}; - -// 자산 랭킹 타입 -export type AssetRankingType = { - topRank: AssetRankItemType[]; - userRank: AssetRankItemType; -}; - -export type RankDataType = { - profitRateRanking: ProfitRankingType; - assetRanking: AssetRankingType; -}; - -// 더미 데이터 -export const dummyRankData: RankDataType = { - profitRateRanking: { - topRank: [ - { - nickname: '투자의신', - profitRate: 356.72, - }, - { - nickname: '주식왕', - profitRate: 245.89, - }, - { - nickname: '워렌버핏', - profitRate: 198.45, - }, - { - nickname: '존버마스터', - profitRate: 156.23, - }, - { - nickname: '주린이탈출', - profitRate: 134.51, - }, - { - nickname: '테슬라홀더', - profitRate: 122.34, - }, - { - nickname: '배당투자자', - profitRate: 98.67, - }, - { - nickname: '단타치는무도가', - profitRate: 87.91, - }, - { - nickname: '가치투자자', - profitRate: 76.45, - }, - { - nickname: '코스피불독', - profitRate: 65.23, - }, - ], - userRank: { - nickname: '나의닉네임', - profitRate: 45.67, - rank: 23, // 23등으로 설정 - }, - }, - assetRanking: { - topRank: [ - { - nickname: '자산왕', - totalAsset: 15800000000, - }, - { - nickname: '억만장자', - totalAsset: 9200000000, - }, - { - nickname: '주식부자', - totalAsset: 7500000000, - }, - { - nickname: '연봉1억', - totalAsset: 6300000000, - }, - { - nickname: '월급쟁이탈출', - totalAsset: 4800000000, - }, - { - nickname: '부자될사람', - totalAsset: 3200000000, - }, - { - nickname: '재테크고수', - totalAsset: 2500000000, - }, - { - nickname: '천만원돌파', - totalAsset: 1800000000, - }, - { - nickname: '주식으로퇴사', - totalAsset: 1200000000, - }, - { - nickname: '투자의시작', - totalAsset: 950000000, - }, - ], - userRank: { - nickname: '나의닉네임', - totalAsset: 850000000, - rank: 15, // 15등으로 설정 - }, - }, -}; diff --git a/FE/src/page/Rank.tsx b/FE/src/page/Rank.tsx index 6c13c715..c12599c2 100644 --- a/FE/src/page/Rank.tsx +++ b/FE/src/page/Rank.tsx @@ -1,15 +1,16 @@ import Nav from 'components/Rank/Nav.tsx'; import RankList from '../components/Rank/RankList.tsx'; -import { dummyRankData } from '../components/Rank/bummyData.ts'; +import { getRanking } from '../service/getRanking.ts'; +import { useQuery } from '@tanstack/react-query'; export default function Rank() { - // const { data, isLoading } = useQuery({ - // queryKey: ['Rank'], - // queryFn: () => getRanking(), - // }); - // - // if (isLoading) return
Loading...
; - const data = dummyRankData; + const { data, isLoading, isError } = useQuery({ + queryKey: ['Rank'], + queryFn: () => getRanking(), + }); + + if (isLoading) return
Loading...
; + if (isError) return
Error!!
; return (
From ae4401500c761c7ca5e490b05344e50e9dd3dc55 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 20:00:28 +0900 Subject: [PATCH 027/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20#177?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 ++ .../bookmark/stock-bookmark.controller.ts | 29 +++++++++++++++++-- .../stock/bookmark/stock-bookmark.service.ts | 18 +++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 40ed1831..d84939d7 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -18,6 +18,7 @@ import { StockTradeHistoryModule } from './stock/trade/history/stock-trade-histo import { RedisModule } from './common/redis/redis.module'; import { HTTPExceptionFilter } from './common/filters/http-exception.filter'; import { RankingModule } from './ranking/ranking.module'; +import { StockBookmarkModule } from './stock/bookmark/stock-bookmark.module'; @Module({ imports: [ @@ -35,6 +36,7 @@ import { RankingModule } from './ranking/ranking.module'; StockTradeHistoryModule, RedisModule, RankingModule, + StockBookmarkModule, ], controllers: [AppController], providers: [ diff --git a/BE/src/stock/bookmark/stock-bookmark.controller.ts b/BE/src/stock/bookmark/stock-bookmark.controller.ts index 20a441e9..b3d9f2e7 100644 --- a/BE/src/stock/bookmark/stock-bookmark.controller.ts +++ b/BE/src/stock/bookmark/stock-bookmark.controller.ts @@ -1,9 +1,34 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { Request } from 'express'; import { StockBookmarkService } from './stock-bookmark.service'; +import { JwtAuthGuard } from '../../auth/jwt-auth-guard'; @Controller('/api/stocks/bookmark') @ApiTags('주식 즐겨찾기 API') export class StockBookmarkController { constructor(private readonly stockBookmarkService: StockBookmarkService) {} + + @Post('/:stockCode') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: '종목 즐겨찾기 등록 API' }) + @ApiResponse({ + status: 201, + description: '종목 즐겨찾기 등록 성공', + }) + async postBookmark( + @Req() request: Request, + @Param('stockCode') stockCode: string, + ) { + await this.stockBookmarkService.registerBookmark( + parseInt(request.user.userId, 10), + stockCode, + ); + } } diff --git a/BE/src/stock/bookmark/stock-bookmark.service.ts b/BE/src/stock/bookmark/stock-bookmark.service.ts index 5e07cf85..a711cb27 100644 --- a/BE/src/stock/bookmark/stock-bookmark.service.ts +++ b/BE/src/stock/bookmark/stock-bookmark.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { StockBookmarkRepository } from './stock-bookmark.repository'; @Injectable() @@ -6,4 +6,20 @@ export class StockBookmarkService { constructor( private readonly stockBookmarkRepository: StockBookmarkRepository, ) {} + + async registerBookmark(userId, stockCode) { + if ( + await this.stockBookmarkRepository.existsBy({ + user_id: userId, + stock_code: stockCode, + }) + ) + throw new BadRequestException('이미 등록된 즐겨찾기입니다.'); + + const bookmark = this.stockBookmarkRepository.create({ + user_id: userId, + stock_code: stockCode, + }); + await this.stockBookmarkRepository.insert(bookmark); + } } From b24753fe60ebf22bfccb72fad99d59fe4ffb2f29 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 20:09:59 +0900 Subject: [PATCH 028/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EC=B7=A8=EC=86=8C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20#178?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bookmark/stock-bookmark.controller.ts | 27 ++++++++++++++++++- .../stock/bookmark/stock-bookmark.service.ts | 17 +++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/BE/src/stock/bookmark/stock-bookmark.controller.ts b/BE/src/stock/bookmark/stock-bookmark.controller.ts index b3d9f2e7..4c776e60 100644 --- a/BE/src/stock/bookmark/stock-bookmark.controller.ts +++ b/BE/src/stock/bookmark/stock-bookmark.controller.ts @@ -1,4 +1,11 @@ -import { Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { + Controller, + Delete, + Param, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, @@ -31,4 +38,22 @@ export class StockBookmarkController { stockCode, ); } + + @Delete('/:stockCode') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: '종목 즐겨찾기 취소 API' }) + @ApiResponse({ + status: 200, + description: '종목 즐겨찾기 취소 성공', + }) + async deleteBookmark( + @Req() request: Request, + @Param('stockCode') stockCode: string, + ) { + await this.stockBookmarkService.unregisterBookmark( + parseInt(request.user.userId, 10), + stockCode, + ); + } } diff --git a/BE/src/stock/bookmark/stock-bookmark.service.ts b/BE/src/stock/bookmark/stock-bookmark.service.ts index a711cb27..49e2a3fa 100644 --- a/BE/src/stock/bookmark/stock-bookmark.service.ts +++ b/BE/src/stock/bookmark/stock-bookmark.service.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { StockBookmarkRepository } from './stock-bookmark.repository'; @Injectable() @@ -22,4 +26,15 @@ export class StockBookmarkService { }); await this.stockBookmarkRepository.insert(bookmark); } + + async unregisterBookmark(userId, stockCode) { + const bookmark = await this.stockBookmarkRepository.findOneBy({ + user_id: userId, + stock_code: stockCode, + }); + + if (!bookmark) throw new NotFoundException('존재하지 않는 북마크입니다.'); + + await this.stockBookmarkRepository.remove(bookmark); + } } From 1b58651dc28e49df6251ce977ae39544ff8c9dc4 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 20:53:25 +0900 Subject: [PATCH 029/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20#17?= =?UTF-8?q?9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/stock-bookmark-response,dto.ts | 37 +++++++++++++++++++ .../bookmark/interface/bookmark.interface.ts | 8 ++++ .../bookmark/stock-bookmark.controller.ts | 15 ++++++++ .../stock/bookmark/stock-bookmark.module.ts | 3 +- .../bookmark/stock-bookmark.repository.ts | 8 ++++ .../stock/bookmark/stock-bookmark.service.ts | 25 +++++++++++++ 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 BE/src/stock/bookmark/dto/stock-bookmark-response,dto.ts create mode 100644 BE/src/stock/bookmark/interface/bookmark.interface.ts diff --git a/BE/src/stock/bookmark/dto/stock-bookmark-response,dto.ts b/BE/src/stock/bookmark/dto/stock-bookmark-response,dto.ts new file mode 100644 index 00000000..e7394cf0 --- /dev/null +++ b/BE/src/stock/bookmark/dto/stock-bookmark-response,dto.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockBookmarkResponseDto { + constructor( + name: string, + code: string, + stck_prpr: string, + prdy_vrss: string, + prdy_vrss_sign: string, + prdy_ctrt: string, + ) { + this.name = name; + this.code = code; + this.stck_prpr = stck_prpr; + this.prdy_vrss = prdy_vrss; + this.prdy_vrss_sign = prdy_vrss_sign; + this.prdy_ctrt = prdy_ctrt; + } + + @ApiProperty({ description: '종목 이름' }) + name: string; + + @ApiProperty({ description: '종목 코드' }) + code: string; + + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; +} diff --git a/BE/src/stock/bookmark/interface/bookmark.interface.ts b/BE/src/stock/bookmark/interface/bookmark.interface.ts new file mode 100644 index 00000000..dc8e45f1 --- /dev/null +++ b/BE/src/stock/bookmark/interface/bookmark.interface.ts @@ -0,0 +1,8 @@ +export interface BookmarkInterface { + b_id: number; + b_user_id: number; + b_stock_code: string; + s_code: string; + s_name: string; + s_market: string; +} diff --git a/BE/src/stock/bookmark/stock-bookmark.controller.ts b/BE/src/stock/bookmark/stock-bookmark.controller.ts index 4c776e60..6830038c 100644 --- a/BE/src/stock/bookmark/stock-bookmark.controller.ts +++ b/BE/src/stock/bookmark/stock-bookmark.controller.ts @@ -1,6 +1,7 @@ import { Controller, Delete, + Get, Param, Post, Req, @@ -56,4 +57,18 @@ export class StockBookmarkController { stockCode, ); } + + @Get() + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: '즐겨찾기 리스트 조회 API' }) + @ApiResponse({ + status: 200, + description: '즐겨찾기 리스트 조회 성공', + }) + async getBookmarkList(@Req() request: Request) { + return this.stockBookmarkService.getBookmarkList( + parseInt(request.user.userId, 10), + ); + } } diff --git a/BE/src/stock/bookmark/stock-bookmark.module.ts b/BE/src/stock/bookmark/stock-bookmark.module.ts index 4296610a..1cec2fac 100644 --- a/BE/src/stock/bookmark/stock-bookmark.module.ts +++ b/BE/src/stock/bookmark/stock-bookmark.module.ts @@ -4,9 +4,10 @@ import { Bookmark } from './stock-bookmark.entity'; import { StockBookmarkController } from './stock-bookmark.controller'; import { StockBookmarkRepository } from './stock-bookmark.repository'; import { StockBookmarkService } from './stock-bookmark.service'; +import { StockDetailModule } from '../detail/stock-detail.module'; @Module({ - imports: [TypeOrmModule.forFeature([Bookmark])], + imports: [TypeOrmModule.forFeature([Bookmark]), StockDetailModule], controllers: [StockBookmarkController], providers: [StockBookmarkRepository, StockBookmarkService], }) diff --git a/BE/src/stock/bookmark/stock-bookmark.repository.ts b/BE/src/stock/bookmark/stock-bookmark.repository.ts index c008180e..99f9e04c 100644 --- a/BE/src/stock/bookmark/stock-bookmark.repository.ts +++ b/BE/src/stock/bookmark/stock-bookmark.repository.ts @@ -2,10 +2,18 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { Bookmark } from './stock-bookmark.entity'; +import { BookmarkInterface } from './interface/bookmark.interface'; @Injectable() export class StockBookmarkRepository extends Repository { constructor(@InjectDataSource() private readonly dataSource: DataSource) { super(Bookmark, dataSource.createEntityManager()); } + + async findBookmarkWithNameByUserId(userId) { + return this.createQueryBuilder('b') + .leftJoinAndSelect('stocks', 's', 's.code = b.stock_code') + .where('b.user_id = :userId', { userId }) + .getRawMany(); + } } diff --git a/BE/src/stock/bookmark/stock-bookmark.service.ts b/BE/src/stock/bookmark/stock-bookmark.service.ts index 49e2a3fa..623f0358 100644 --- a/BE/src/stock/bookmark/stock-bookmark.service.ts +++ b/BE/src/stock/bookmark/stock-bookmark.service.ts @@ -4,11 +4,14 @@ import { NotFoundException, } from '@nestjs/common'; import { StockBookmarkRepository } from './stock-bookmark.repository'; +import { StockDetailService } from '../detail/stock-detail.service'; +import { StockBookmarkResponseDto } from './dto/stock-bookmark-response,dto'; @Injectable() export class StockBookmarkService { constructor( private readonly stockBookmarkRepository: StockBookmarkRepository, + private readonly stockDetailService: StockDetailService, ) {} async registerBookmark(userId, stockCode) { @@ -37,4 +40,26 @@ export class StockBookmarkService { await this.stockBookmarkRepository.remove(bookmark); } + + async getBookmarkList(userId) { + const bookmarks = + await this.stockBookmarkRepository.findBookmarkWithNameByUserId(userId); + + return Promise.all( + bookmarks.map(async (bookmark) => { + const detail = await this.stockDetailService.getInquirePrice( + bookmark.b_stock_code, + ); + + return new StockBookmarkResponseDto( + bookmark.s_name, + bookmark.b_stock_code, + detail.stck_prpr, + detail.prdy_vrss, + detail.prdy_vrss_sign, + detail.prdy_ctrt, + ); + }), + ); + } } From 17bf02f084e5da580095f98edae24a87f7bef616 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 20:56:58 +0900 Subject: [PATCH 030/128] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=EC=A6=90?= =?UTF-8?q?=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20swagger=20type=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#179?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/bookmark/stock-bookmark.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BE/src/stock/bookmark/stock-bookmark.controller.ts b/BE/src/stock/bookmark/stock-bookmark.controller.ts index 6830038c..f81fa4fc 100644 --- a/BE/src/stock/bookmark/stock-bookmark.controller.ts +++ b/BE/src/stock/bookmark/stock-bookmark.controller.ts @@ -16,6 +16,7 @@ import { import { Request } from 'express'; import { StockBookmarkService } from './stock-bookmark.service'; import { JwtAuthGuard } from '../../auth/jwt-auth-guard'; +import { StockBookmarkResponseDto } from './dto/stock-bookmark-response,dto'; @Controller('/api/stocks/bookmark') @ApiTags('주식 즐겨찾기 API') @@ -65,6 +66,7 @@ export class StockBookmarkController { @ApiResponse({ status: 200, description: '즐겨찾기 리스트 조회 성공', + type: [StockBookmarkResponseDto], }) async getBookmarkList(@Req() request: Request) { return this.stockBookmarkService.getBookmarkList( From 7cdf33528376a7d5cd60ed140cb5de3165791229 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 25 Nov 2024 21:00:08 +0900 Subject: [PATCH 031/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=83=81=ED=83=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 604c9be9..eaa5e51f 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -164,6 +164,8 @@ jobs: --env-file ${{matrix.app.name}}.env \ ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + docker ps | grep ${{ matrix.app.container }} + docker logs ${{ matrix.app.container }} rm ${{matrix.app.name}}.env - name: Remove Github Action Ip to Security group From 8f49478966a613833e244cdcbaf73bcdcdefbe55 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 25 Nov 2024 21:03:08 +0900 Subject: [PATCH 032/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=A0=95?= =?UTF-8?q?=20=EC=A2=85=EB=AA=A9=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=A6=90?= =?UTF-8?q?=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EB=93=B1=EB=A1=9D=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20#17?= =?UTF-8?q?9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bookmark/stock-bookmark.controller.ts | 19 +++++++++++++++++++ .../stock/bookmark/stock-bookmark.service.ts | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/BE/src/stock/bookmark/stock-bookmark.controller.ts b/BE/src/stock/bookmark/stock-bookmark.controller.ts index f81fa4fc..886f5e79 100644 --- a/BE/src/stock/bookmark/stock-bookmark.controller.ts +++ b/BE/src/stock/bookmark/stock-bookmark.controller.ts @@ -73,4 +73,23 @@ export class StockBookmarkController { parseInt(request.user.userId, 10), ); } + + @Get('/:stockCode') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: '특정 종목에 대한 즐겨찾기 등록 여부 조회 API' }) + @ApiResponse({ + status: 200, + description: '즐겨찾기 등록 여부 조회 성공', + example: { is_bookmarked: true }, + }) + async getBookmarkActive( + @Req() request: Request, + @Param('stockCode') stockCode: string, + ) { + return this.stockBookmarkService.getBookmarkActive( + parseInt(request.user.userId, 10), + stockCode, + ); + } } diff --git a/BE/src/stock/bookmark/stock-bookmark.service.ts b/BE/src/stock/bookmark/stock-bookmark.service.ts index 623f0358..17710c39 100644 --- a/BE/src/stock/bookmark/stock-bookmark.service.ts +++ b/BE/src/stock/bookmark/stock-bookmark.service.ts @@ -62,4 +62,13 @@ export class StockBookmarkService { }), ); } + + async getBookmarkActive(userId, stockCode) { + return { + is_bookmarked: await this.stockBookmarkRepository.existsBy({ + user_id: userId, + stock_code: stockCode, + }), + }; + } } From 95884b802710b58a18af2c4e76f6777fd115b68b Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 26 Nov 2024 12:31:43 +0900 Subject: [PATCH 033/128] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=ED=8F=89=EA=B7=A0=EC=84=A0=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20dto=EC=97=90=20=EB=8B=B4=EC=95=84=EC=84=9C=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/dto/stock-detail-chart-data.dto.ts | 6 ++++ BE/src/stock/detail/stock-detail.service.ts | 32 +++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts index de68279f..af2a256f 100644 --- a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts @@ -21,4 +21,10 @@ export class InquirePriceChartDataDto { @ApiProperty({ description: '전일 대비 부호' }) prdy_vrss_sign: string; + + @ApiProperty({ description: '이동 평균선 데이터(5일)' }) + mov_avg_5: string; + + @ApiProperty({ description: '이동 평균선 데이터(20일)' }) + mov_avg_20: string; } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index b3e537ed..d26a70a2 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -82,6 +82,14 @@ export class StockDetailService { date2: string, periodDivCode: string, ) { + if (date1 === '') { + const today = new Date(); + const pervDay = new Date(); + pervDay.setDate(today.getDate() - 150); + date2 = new Date().toISOString().slice(0, 10).replace(/-/g, ''); + date1 = pervDay.toISOString().slice(0, 10).replace(/-/g, ''); + } + const queryParams = { fid_cond_mrkt_div_code: 'J', fid_input_iscd: stockCode, @@ -98,7 +106,7 @@ export class StockDetailService { queryParams, ); - return this.formatStockInquirePriceData(response).slice().reverse(); + return this.formatStockInquirePriceData(response).slice(-30); } /** @@ -111,7 +119,13 @@ export class StockDetailService { private formatStockInquirePriceData(response: InquirePriceChartApiResponse) { const { output2 } = response; - return output2.map((info) => { + output2.sort((a, b) => { + if (a.stck_bsop_date > b.stck_bsop_date) return 1; + if (a.stck_bsop_date < b.stck_bsop_date) return -1; + return 0; + }); + + return output2.map((info, index) => { const stockData = new InquirePriceChartDataDto(); const { stck_bsop_date, @@ -131,6 +145,20 @@ export class StockDetailService { stockData.acml_vol = acml_vol; stockData.prdy_vrss_sign = prdy_vrss_sign; + if (index >= 4) { + const movAvg5 = output2 + .slice(index - 4, index + 1) + .reduce((acc, cur) => acc + Number(cur.stck_clpr), 0); + stockData.mov_avg_5 = (movAvg5 / 5).toFixed(2); + } + + if (index >= 19) { + const movAvg20 = output2 + .slice(index - 19, index + 1) + .reduce((acc, cur) => acc + Number(cur.stck_clpr), 0); + stockData.mov_avg_20 = (movAvg20 / 20).toFixed(2); + } + return stockData; }); } From 5c5408e04c94221d7941aa0c5ef24bfb198436b7 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 26 Nov 2024 12:36:43 +0900 Subject: [PATCH 034/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=8B=9C=EC=9E=91=EC=9D=BC=20=EB=B3=80=EA=B2=BD(#1?= =?UTF-8?q?83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index d26a70a2..0937c9bf 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -85,7 +85,7 @@ export class StockDetailService { if (date1 === '') { const today = new Date(); const pervDay = new Date(); - pervDay.setDate(today.getDate() - 150); + pervDay.setDate(today.getDate() - 365); date2 = new Date().toISOString().slice(0, 10).replace(/-/g, ''); date1 = pervDay.toISOString().slice(0, 10).replace(/-/g, ''); } From 93c569f1a68bc584857e176ce911b6c8fd349c6a Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 26 Nov 2024 13:10:35 +0900 Subject: [PATCH 035/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20lint=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9D=BC?= =?UTF-8?q?=EA=B0=84=20=EC=B0=A8=ED=8A=B8,=20=EC=9B=94=EB=B3=84=20?= =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EC=9D=BC=EC=9E=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 2 +- BE/src/stock/detail/stock-detail.service.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index edf1d064..5cdd0a05 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -98,7 +98,7 @@ export class RankingService { return { topRank: parsedTopRank, - userRank: userRank, + userRank, }; } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 0937c9bf..2f830cea 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -82,19 +82,24 @@ export class StockDetailService { date2: string, periodDivCode: string, ) { + let newDate1 = date1; + let newDate2 = date2; + if (date1 === '') { const today = new Date(); - const pervDay = new Date(); - pervDay.setDate(today.getDate() - 365); - date2 = new Date().toISOString().slice(0, 10).replace(/-/g, ''); - date1 = pervDay.toISOString().slice(0, 10).replace(/-/g, ''); + const prevDay = new Date(); + if (periodDivCode === 'D') prevDay.setDate(today.getDate() - 60); + if (periodDivCode === 'M') prevDay.setDate(today.getDate() - 1200); + prevDay.setDate(today.getDate() - 365); + newDate2 = new Date().toISOString().slice(0, 10).replace(/-/g, ''); + newDate1 = prevDay.toISOString().slice(0, 10).replace(/-/g, ''); } const queryParams = { fid_cond_mrkt_div_code: 'J', fid_input_iscd: stockCode, - fid_input_date_1: date1, - fid_input_date_2: date2, + fid_input_date_1: newDate1, + fid_input_date_2: newDate2, fid_period_div_code: periodDivCode, fid_org_adj_prc: '0', }; From 0053da9b9f4cd7c3818c5e57e41fea3304afdf08 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 26 Nov 2024 13:19:43 +0900 Subject: [PATCH 036/128] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20=EC=97=B0?= =?UTF-8?q?=EA=B0=84=20=EC=A3=BC=EC=8B=9D=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EC=88=98=EC=A0=95(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 2f830cea..d0b52769 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -90,6 +90,7 @@ export class StockDetailService { const prevDay = new Date(); if (periodDivCode === 'D') prevDay.setDate(today.getDate() - 60); if (periodDivCode === 'M') prevDay.setDate(today.getDate() - 1200); + if (periodDivCode === 'Y') prevDay.setDate(today.getDate() - 20000); prevDay.setDate(today.getDate() - 365); newDate2 = new Date().toISOString().slice(0, 10).replace(/-/g, ''); newDate1 = prevDay.toISOString().slice(0, 10).replace(/-/g, ''); From feca84cf29dd14295a6608af4e0c064277665f4f Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 13:54:59 +0900 Subject: [PATCH 037/128] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20stockS?= =?UTF-8?q?ocket=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=EC=97=90=20stock-i?= =?UTF-8?q?ndex-socket.service,=20stock-price-socket.service=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=B4=EC=84=9C=20=EA=B4=80=EB=A6=AC=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base-stock-socket.domain-service.ts | 24 +++ .../stock-execute-order.repository.ts | 105 +++++++++++++ .../stockSocket/stock-index-socket.service.ts | 41 +++++ .../stockSocket/stock-price-socket.service.ts | 145 ++++++++++++++++++ BE/src/stockSocket/stock-socket.module.ts | 16 ++ 5 files changed, 331 insertions(+) create mode 100644 BE/src/stockSocket/base-stock-socket.domain-service.ts create mode 100644 BE/src/stockSocket/stock-execute-order.repository.ts create mode 100644 BE/src/stockSocket/stock-index-socket.service.ts create mode 100644 BE/src/stockSocket/stock-price-socket.service.ts create mode 100644 BE/src/stockSocket/stock-socket.module.ts diff --git a/BE/src/stockSocket/base-stock-socket.domain-service.ts b/BE/src/stockSocket/base-stock-socket.domain-service.ts new file mode 100644 index 00000000..6bd5b571 --- /dev/null +++ b/BE/src/stockSocket/base-stock-socket.domain-service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { SocketGateway } from '../common/websocket/socket.gateway'; +import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; + +@Injectable() +export abstract class BaseStockSocketDomainService { + protected constructor( + protected readonly socketGateway: SocketGateway, + protected readonly baseSocketDomainService: BaseSocketDomainService, + protected readonly TR_ID: string, + ) { + baseSocketDomainService.registerSocketOpenHandler(() => + this.socketOpenHandler(), + ); + + baseSocketDomainService.registerSocketDataHandler(TR_ID, (data: string[]) => + this.socketDataHandler(data), + ); + } + + abstract socketOpenHandler(): void | Promise; + + abstract socketDataHandler(data: string[]): void; +} diff --git a/BE/src/stockSocket/stock-execute-order.repository.ts b/BE/src/stockSocket/stock-execute-order.repository.ts new file mode 100644 index 00000000..3a0eca92 --- /dev/null +++ b/BE/src/stockSocket/stock-execute-order.repository.ts @@ -0,0 +1,105 @@ +import { DataSource, Repository } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { InternalServerErrorException } from '@nestjs/common'; +import { Order } from '../stock/order/stock-order.entity'; +import { StatusType } from '../stock/order/enum/status-type'; +import { Asset } from '../asset/asset.entity'; +import { UserStock } from '../asset/user-stock.entity'; + +export class StockExecuteOrderRepository extends Repository { + constructor(@InjectDataSource() private dataSource: DataSource) { + super(Order, dataSource.createEntityManager()); + } + + async findAllCodeByStatus() { + return this.createQueryBuilder('orders') + .select('DISTINCT orders.stock_code') + .where({ status: StatusType.PENDING }) + .getRawMany(); + } + + async updateOrderAndAssetAndUserStockWhenBuy(order, realPrice) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => `cash_balance - :realPrice`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameters({ realPrice }) + .execute(); + + await queryRunner.query( + `INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE avg_price = (avg_price * quantity + ? * ?) / (quantity + ?), quantity = quantity + ?`, + [ + order.user_id, + order.stock_code, + order.amount, + order.price, + new Date(), + order.price, + order.amount, + order.amount, + order.amount, + ], + ); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(err); + } finally { + await queryRunner.release(); + } + } + + async updateOrderAndAssetAndUserStockWhenSell(order, realPrice) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => `cash_balance + :realPrice`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameters({ realPrice }) + .execute(); + + await queryRunner.manager + .createQueryBuilder() + .update(UserStock) + .set({ + quantity: () => `quantity - :newQuantity`, + }) + .where({ user_id: order.user_id, stock_code: order.stock_code }) + .setParameters({ newQuantity: order.amount }) + .execute(); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } + } +} diff --git a/BE/src/stockSocket/stock-index-socket.service.ts b/BE/src/stockSocket/stock-index-socket.service.ts new file mode 100644 index 00000000..208fc71f --- /dev/null +++ b/BE/src/stockSocket/stock-index-socket.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; +import { BaseStockSocketDomainService } from './base-stock-socket.domain-service'; +import { SocketGateway } from '../common/websocket/socket.gateway'; +import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; + +@Injectable() +export class StockIndexSocketService extends BaseStockSocketDomainService { + private STOCK_CODE = { + '0001': 'KOSPI', + '1001': 'KOSDAQ', + '2001': 'KOSPI200', + '3003': 'KSQ150', + }; + + constructor( + protected readonly socketGateway: SocketGateway, + protected readonly baseSocketDomainService: BaseSocketDomainService, + ) { + super(socketGateway, baseSocketDomainService, 'H0UPCNT0'); + } + + socketOpenHandler(): void { + this.baseSocketDomainService.registerCode(this.TR_ID, '0001'); // 코스피 + this.baseSocketDomainService.registerCode(this.TR_ID, '1001'); // 코스닥 + this.baseSocketDomainService.registerCode(this.TR_ID, '2001'); // 코스피200 + this.baseSocketDomainService.registerCode(this.TR_ID, '3003'); // KSQ150 + } + + socketDataHandler(data: string[]): void { + this.socketGateway.sendStockIndexValueToClient( + this.STOCK_CODE[data[0]], + new StockIndexValueElementDto( + data[2], // 주가 지수 + data[4], // 전일 대비 등락 + data[9], // 전일 대비 등락률 + data[3], // 부호 + ), + ); + } +} diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts new file mode 100644 index 00000000..60695e13 --- /dev/null +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -0,0 +1,145 @@ +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { BaseSocketDomainService } from '../common/websocket/base-socket.domain-service'; +import { SocketGateway } from '../common/websocket/socket.gateway'; +import { BaseStockSocketDomainService } from './base-stock-socket.domain-service'; +import { Order } from '../stock/order/stock-order.entity'; +import { TradeType } from '../stock/order/enum/trade-type'; +import { StatusType } from '../stock/order/enum/status-type'; +import { TodayStockTradeHistoryDataDto } from '../stock/trade/history/dto/today-stock-trade-history-data.dto'; +import { StockDetailSocketDataDto } from '../stock/trade/history/dto/stock-detail-socket-data.dto'; +import { StockExecuteOrderRepository } from './stock-execute-order.repository'; + +@Injectable() +export class StockPriceSocketService extends BaseStockSocketDomainService { + private readonly logger = new Logger(); + + constructor( + protected readonly socketGateway: SocketGateway, + protected readonly baseSocketDomainService: BaseSocketDomainService, + private readonly stockExecuteOrderRepository: StockExecuteOrderRepository, + ) { + super(socketGateway, baseSocketDomainService, 'H0STCNT0'); + } + + async socketOpenHandler(): Promise { + const orders: Order[] = + await this.stockExecuteOrderRepository.findAllCodeByStatus(); + orders.forEach((order) => { + this.baseSocketDomainService.registerCode(this.TR_ID, order.stock_code); + }); + } + + socketDataHandler(data: string[]) { + this.checkExecutableOrder( + data[0], // 주식 코드 + data[2], // 주식 체결가 + ).catch((err) => { + throw new InternalServerErrorException(err); + }); + + const tradeData: TodayStockTradeHistoryDataDto = { + stck_shrn_iscd: data[0], + stck_cntg_hour: data[1], + stck_prpr: data[2], + prdy_vrss_sign: data[3], + cntg_vol: data[12], + prdy_ctrt: data[5], + }; + + const detailData: StockDetailSocketDataDto = { + stck_prpr: data[2], + prdy_vrss_sign: data[3], + prdy_vrss: data[4], + prdy_ctrt: data[5], + }; + + this.socketGateway.sendStockIndexValueToClient( + `trade-history/${data[0]}`, + tradeData, + ); + + this.socketGateway.sendStockIndexValueToClient( + `detail/${data[0]}`, + detailData, + ); + } + + subscribeByCode(trKey: string) { + this.baseSocketDomainService.registerCode(this.TR_ID, trKey); + } + + unsubscribeByCode(trKey: string) { + this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); + } + + private async checkExecutableOrder(stockCode: string, value) { + const buyOrders = await this.stockExecuteOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.BUY, + status: StatusType.PENDING, + price: MoreThanOrEqual(value), + }, + }); + + const sellOrders = await this.stockExecuteOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.SELL, + status: StatusType.PENDING, + price: LessThanOrEqual(value), + }, + }); + + await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); + await Promise.all( + sellOrders.map((sellOrder) => this.executeSell(sellOrder)), + ); + + if ( + !(await this.stockExecuteOrderRepository.existsBy({ + stock_code: stockCode, + status: StatusType.PENDING, + })) + ) + this.unsubscribeByCode(stockCode); + } + + private async executeBuy(order) { + this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockExecuteOrderRepository.updateOrderAndAssetAndUserStockWhenBuy( + order, + totalPrice + fee, + ); + } + + private async executeSell(order) { + this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockExecuteOrderRepository.updateOrderAndAssetAndUserStockWhenSell( + order, + totalPrice - fee, + ); + } + + private calculateFee(totalPrice: number) { + if (totalPrice <= 10000000) return Math.floor(totalPrice * 0.16); + if (totalPrice > 10000000 && totalPrice <= 50000000) + return Math.floor(totalPrice * 0.14); + if (totalPrice > 50000000 && totalPrice <= 100000000) + return Math.floor(totalPrice * 0.12); + if (totalPrice > 100000000 && totalPrice <= 300000000) + return Math.floor(totalPrice * 0.1); + return Math.floor(totalPrice * 0.08); + } +} diff --git a/BE/src/stockSocket/stock-socket.module.ts b/BE/src/stockSocket/stock-socket.module.ts new file mode 100644 index 00000000..fd2dc094 --- /dev/null +++ b/BE/src/stockSocket/stock-socket.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { StockIndexSocketService } from './stock-index-socket.service'; +import { StockPriceSocketService } from './stock-price-socket.service'; +import { SocketModule } from '../common/websocket/socket.module'; +import { StockExecuteOrderRepository } from './stock-execute-order.repository'; + +@Module({ + imports: [SocketModule], + providers: [ + StockIndexSocketService, + StockPriceSocketService, + StockExecuteOrderRepository, + ], + exports: [StockIndexSocketService, StockPriceSocketService], +}) +export class StockSocketModule {} From c1a1f0044cddbbde16d821b598b50c5c59ae2842 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 13:57:52 +0900 Subject: [PATCH 038/128] =?UTF-8?q?=E2=99=BB=20refactor:=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=EC=84=9C=20sto?= =?UTF-8?q?ckSocket=20=EB=AA=A8=EB=93=88=EC=9D=98=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=B0=B8=EC=A1=B0=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.module.ts | 4 +- BE/src/asset/asset.service.ts | 10 +- BE/src/common/websocket/socket.gateway.ts | 4 - .../stock/index/stock-index-socket.service.ts | 42 ----- BE/src/stock/index/stock-index.module.ts | 3 +- .../stock/order/stock-order-socket.service.ts | 119 ------------- BE/src/stock/order/stock-order.module.ts | 11 +- BE/src/stock/order/stock-order.repository.ts | 85 --------- BE/src/stock/order/stock-order.service.ts | 12 +- .../stock-trade-history-socket.service.ts | 162 ------------------ .../history/stock-trade-history.controller.ts | 73 ++++---- .../history/stock-trade-history.module.ts | 7 +- .../history/stock-trade-history.service.ts | 7 + 13 files changed, 63 insertions(+), 476 deletions(-) delete mode 100644 BE/src/stock/index/stock-index-socket.service.ts delete mode 100644 BE/src/stock/order/stock-order-socket.service.ts delete mode 100644 BE/src/stock/trade/history/stock-trade-history-socket.service.ts diff --git a/BE/src/asset/asset.module.ts b/BE/src/asset/asset.module.ts index db1dc8ad..de73a73e 100644 --- a/BE/src/asset/asset.module.ts +++ b/BE/src/asset/asset.module.ts @@ -7,13 +7,13 @@ import { Asset } from './asset.entity'; import { UserStock } from './user-stock.entity'; import { UserStockRepository } from './user-stock.repository'; import { StockDetailModule } from '../stock/detail/stock-detail.module'; -import { StockTradeHistoryModule } from '../stock/trade/history/stock-trade-history.module'; +import { StockSocketModule } from '../stockSocket/stock-socket.module'; @Module({ imports: [ TypeOrmModule.forFeature([Asset, UserStock]), StockDetailModule, - StockTradeHistoryModule, + StockSocketModule, ], controllers: [AssetController], providers: [AssetService, AssetRepository, UserStockRepository], diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts index 2471c29b..ce4a8f88 100644 --- a/BE/src/asset/asset.service.ts +++ b/BE/src/asset/asset.service.ts @@ -8,8 +8,8 @@ import { StockDetailService } from '../stock/detail/stock-detail.service'; import { UserStock } from './user-stock.entity'; import { Asset } from './asset.entity'; import { InquirePriceResponseDto } from '../stock/detail/dto/stock-detail-response.dto'; -import { StockTradeHistorySocketService } from '../stock/trade/history/stock-trade-history-socket.service'; import { TradeType } from '../stock/order/enum/trade-type'; +import { StockPriceSocketService } from '../stockSocket/stock-price-socket.service'; @Injectable() export class AssetService { @@ -17,7 +17,7 @@ export class AssetService { private readonly userStockRepository: UserStockRepository, private readonly assetRepository: AssetRepository, private readonly stockDetailService: StockDetailService, - private readonly stockTradeHistorySocketService: StockTradeHistorySocketService, + private readonly stockPriceSocketService: StockPriceSocketService, ) {} async getUserStockByCode(userId: number, stockCode: string) { @@ -148,7 +148,7 @@ export class AssetService { await this.userStockRepository.findAllDistinctCode(userId); userStocks.map((userStock) => - this.stockTradeHistorySocketService.subscribeByCode(userStock.stock_code), + this.stockPriceSocketService.subscribeByCode(userStock.stock_code), ); } @@ -157,9 +157,7 @@ export class AssetService { await this.userStockRepository.findAllDistinctCode(userId); userStocks.map((userStock) => - this.stockTradeHistorySocketService.unsubscribeByCode( - userStock.stock_code, - ), + this.stockPriceSocketService.unsubscribeByCode(userStock.stock_code), ); } } diff --git a/BE/src/common/websocket/socket.gateway.ts b/BE/src/common/websocket/socket.gateway.ts index 48668008..d533c3f0 100644 --- a/BE/src/common/websocket/socket.gateway.ts +++ b/BE/src/common/websocket/socket.gateway.ts @@ -20,8 +20,4 @@ export class SocketGateway { this.server.emit(event, stockIndexValue); } - - sendStockTradeHistoryValueToClient(event, historyData) { - this.server.emit(event, historyData); - } } diff --git a/BE/src/stock/index/stock-index-socket.service.ts b/BE/src/stock/index/stock-index-socket.service.ts deleted file mode 100644 index 05ecb307..00000000 --- a/BE/src/stock/index/stock-index-socket.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { StockIndexValueElementDto } from './dto/stock-index-value-element.dto'; -import { BaseSocketDomainService } from '../../common/websocket/base-socket.domain-service'; -import { SocketGateway } from '../../common/websocket/socket.gateway'; - -@Injectable() -export class StockIndexSocketService { - private TR_ID = 'H0UPCNT0'; - private STOCK_CODE = { - '0001': 'KOSPI', - '1001': 'KOSDAQ', - '2001': 'KOSPI200', - '3003': 'KSQ150', - }; - - constructor( - private readonly socketGateway: SocketGateway, - private readonly baseSocketDomainService: BaseSocketDomainService, - ) { - baseSocketDomainService.registerSocketOpenHandler(() => { - this.baseSocketDomainService.registerCode(this.TR_ID, '0001'); // 코스피 - this.baseSocketDomainService.registerCode(this.TR_ID, '1001'); // 코스닥 - this.baseSocketDomainService.registerCode(this.TR_ID, '2001'); // 코스피200 - this.baseSocketDomainService.registerCode(this.TR_ID, '3003'); // KSQ150 - }); - - baseSocketDomainService.registerSocketDataHandler( - this.TR_ID, - (data: string[]) => { - this.socketGateway.sendStockIndexValueToClient( - this.STOCK_CODE[data[0]], - new StockIndexValueElementDto( - data[2], // 주가 지수 - data[4], // 전일 대비 등락 - data[9], // 전일 대비 등락률 - data[3], // 부호 - ), - ); - }, - ); - } -} diff --git a/BE/src/stock/index/stock-index.module.ts b/BE/src/stock/index/stock-index.module.ts index dd3d21f4..186a5b05 100644 --- a/BE/src/stock/index/stock-index.module.ts +++ b/BE/src/stock/index/stock-index.module.ts @@ -3,11 +3,10 @@ import { StockIndexController } from './stock-index.controller'; import { StockIndexService } from './stock-index.service'; import { KoreaInvestmentModule } from '../../common/koreaInvestment/korea-investment.module'; import { SocketModule } from '../../common/websocket/socket.module'; -import { StockIndexSocketService } from './stock-index-socket.service'; @Module({ imports: [KoreaInvestmentModule, SocketModule], controllers: [StockIndexController], - providers: [StockIndexService, StockIndexSocketService], + providers: [StockIndexService], }) export class StockIndexModule {} diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts deleted file mode 100644 index 5b1b5d5a..00000000 --- a/BE/src/stock/order/stock-order-socket.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - Injectable, - InternalServerErrorException, - Logger, -} from '@nestjs/common'; -import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; -import { BaseSocketDomainService } from '../../common/websocket/base-socket.domain-service'; -import { SocketGateway } from '../../common/websocket/socket.gateway'; -import { Order } from './stock-order.entity'; -import { TradeType } from './enum/trade-type'; -import { StatusType } from './enum/status-type'; -import { StockOrderRepository } from './stock-order.repository'; - -@Injectable() -export class StockOrderSocketService { - private TR_ID = 'H0STCNT0'; - - private readonly logger = new Logger(); - - constructor( - private readonly socketGateway: SocketGateway, - private readonly baseSocketDomainService: BaseSocketDomainService, - private readonly stockOrderRepository: StockOrderRepository, - ) { - baseSocketDomainService.registerSocketOpenHandler(async () => { - const orders: Order[] = - await this.stockOrderRepository.findAllCodeByStatus(); - orders.forEach((order) => { - baseSocketDomainService.registerCode(this.TR_ID, order.stock_code); - }); - }); - - baseSocketDomainService.registerSocketDataHandler( - this.TR_ID, - (data: string[]) => { - this.checkExecutableOrder( - data[0], // 주식 코드 - data[2], // 주식 체결가 - ).catch((err) => { - throw new InternalServerErrorException(err); - }); - }, - ); - } - - subscribeByCode(trKey: string) { - this.baseSocketDomainService.registerCode(this.TR_ID, trKey); - } - - unsubscribeByCode(trKey: string) { - this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); - } - - private async checkExecutableOrder(stockCode: string, value) { - const buyOrders = await this.stockOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.BUY, - status: StatusType.PENDING, - price: MoreThanOrEqual(value), - }, - }); - - const sellOrders = await this.stockOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.SELL, - status: StatusType.PENDING, - price: LessThanOrEqual(value), - }, - }); - - await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); - await Promise.all( - sellOrders.map((sellOrder) => this.executeSell(sellOrder)), - ); - - if ( - !(await this.stockOrderRepository.existsBy({ - stock_code: stockCode, - status: StatusType.PENDING, - })) - ) - this.unsubscribeByCode(stockCode); - } - - private async executeBuy(order) { - this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetAndUserStockWhenBuy( - order, - totalPrice + fee, - ); - } - - private async executeSell(order) { - this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetAndUserStockWhenSell( - order, - totalPrice - fee, - ); - } - - private calculateFee(totalPrice: number) { - if (totalPrice <= 10000000) return Math.floor(totalPrice * 0.16); - if (totalPrice > 10000000 && totalPrice <= 50000000) - return Math.floor(totalPrice * 0.14); - if (totalPrice > 50000000 && totalPrice <= 100000000) - return Math.floor(totalPrice * 0.12); - if (totalPrice > 100000000 && totalPrice <= 300000000) - return Math.floor(totalPrice * 0.1); - return Math.floor(totalPrice * 0.08); - } -} diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index 375d4a53..69bda237 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -6,11 +6,16 @@ import { Order } from './stock-order.entity'; import { StockOrderRepository } from './stock-order.repository'; import { SocketModule } from '../../common/websocket/socket.module'; import { AssetModule } from '../../asset/asset.module'; -import { StockOrderSocketService } from './stock-order-socket.service'; +import { StockSocketModule } from '../../stockSocket/stock-socket.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order]), SocketModule, AssetModule], + imports: [ + TypeOrmModule.forFeature([Order]), + SocketModule, + AssetModule, + StockSocketModule, + ], controllers: [StockOrderController], - providers: [StockOrderService, StockOrderRepository, StockOrderSocketService], + providers: [StockOrderService, StockOrderRepository], }) export class StockOrderModule {} diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 92da1ce6..84c453ee 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -20,91 +20,6 @@ export class StockOrderRepository extends Repository { .getRawMany(); } - async updateOrderAndAssetAndUserStockWhenBuy(order, realPrice) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction(); - - try { - await queryRunner.manager.update( - Order, - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, - ); - await queryRunner.manager - .createQueryBuilder() - .update(Asset) - .set({ - cash_balance: () => `cash_balance - :realPrice`, - last_updated: new Date(), - }) - .where({ user_id: order.user_id }) - .setParameters({ realPrice }) - .execute(); - - await queryRunner.query( - `INSERT INTO user_stocks (user_id, stock_code, quantity, avg_price, last_updated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE avg_price = (avg_price * quantity + ? * ?) / (quantity + ?), quantity = quantity + ?`, - [ - order.user_id, - order.stock_code, - order.amount, - order.price, - new Date(), - order.price, - order.amount, - order.amount, - order.amount, - ], - ); - - await queryRunner.commitTransaction(); - } catch (err) { - await queryRunner.rollbackTransaction(); - throw new InternalServerErrorException(err); - } finally { - await queryRunner.release(); - } - } - - async updateOrderAndAssetAndUserStockWhenSell(order, realPrice) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction(); - - try { - await queryRunner.manager.update( - Order, - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, - ); - await queryRunner.manager - .createQueryBuilder() - .update(Asset) - .set({ - cash_balance: () => `cash_balance + :realPrice`, - last_updated: new Date(), - }) - .where({ user_id: order.user_id }) - .setParameters({ realPrice }) - .execute(); - - await queryRunner.manager - .createQueryBuilder() - .update(UserStock) - .set({ - quantity: () => `quantity - :newQuantity`, - }) - .where({ user_id: order.user_id, stock_code: order.stock_code }) - .setParameters({ newQuantity: order.amount }) - .execute(); - - await queryRunner.commitTransaction(); - } catch (err) { - await queryRunner.rollbackTransaction(); - throw new InternalServerErrorException(); - } finally { - await queryRunner.release(); - } - } - async findAllPendingOrdersByUserId(userId: number) { return this.createQueryBuilder('o') .leftJoinAndSelect('stocks', 's', 's.code = o.stock_code') diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index bbc4572f..73879bca 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -9,17 +9,17 @@ import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; import { StatusType } from './enum/status-type'; -import { StockOrderSocketService } from './stock-order-socket.service'; import { UserStockRepository } from '../../asset/user-stock.repository'; import { AssetRepository } from '../../asset/asset.repository'; import { StockOrderElementResponseDto } from './dto/stock-order-element-response.dto'; import { Order } from './stock-order.entity'; +import { StockPriceSocketService } from '../../stockSocket/stock-price-socket.service'; @Injectable() export class StockOrderService { constructor( private readonly stockOrderRepository: StockOrderRepository, - private readonly stockOrderSocketService: StockOrderSocketService, + private readonly stockPriceSocketService: StockPriceSocketService, private readonly userStockRepository: UserStockRepository, private readonly assetRepository: AssetRepository, ) {} @@ -54,7 +54,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); - this.stockOrderSocketService.subscribeByCode(stockOrderRequest.stock_code); + this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); } async sell(userId: number, stockOrderRequest: StockOrderRequestDto) { @@ -89,7 +89,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); - this.stockOrderSocketService.subscribeByCode(stockOrderRequest.stock_code); + this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); } async cancel(userId: number, orderId: number) { @@ -111,7 +111,7 @@ export class StockOrderService { status: StatusType.PENDING, })) ) - this.stockOrderSocketService.unsubscribeByCode(order.stock_code); + this.stockPriceSocketService.unsubscribeByCode(order.stock_code); } async getPendingListByUserId(userId: number) { @@ -137,7 +137,7 @@ export class StockOrderService { await Promise.all( orders.map((order) => - this.stockOrderSocketService.unsubscribeByCode(order.stock_code), + this.stockPriceSocketService.unsubscribeByCode(order.stock_code), ), ); diff --git a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts b/BE/src/stock/trade/history/stock-trade-history-socket.service.ts deleted file mode 100644 index b32f9601..00000000 --- a/BE/src/stock/trade/history/stock-trade-history-socket.service.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { WebSocket } from 'ws'; -import axios from 'axios'; -import { filter, map, Observable, Subject } from 'rxjs'; -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { SseEvent } from './interface/sse-event'; -import { SocketConnectTokenInterface } from '../../../common/websocket/interface/socket.interface'; -import { getFullTestURL } from '../../../util/get-full-URL'; -import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; -import { SocketGateway } from '../../../common/websocket/socket.gateway'; -import { StockDetailSocketDataDto } from './dto/stock-detail-socket-data.dto'; - -@Injectable() -export class StockTradeHistorySocketService implements OnModuleInit { - private readonly logger = new Logger(''); - private socket: WebSocket; - private socketConnectionKey: string; - private subscribedStocks = new Set(); - private TR_ID = 'H0STCNT0'; - private eventSubject = new Subject(); - - constructor(private readonly socketGateway: SocketGateway) {} - - async onModuleInit() { - this.socketConnectionKey = await this.getSocketConnectionKey(); - this.socket = new WebSocket(process.env.KOREA_INVESTMENT_TEST_SOCKET_URL); - - this.socket.onopen = () => {}; - - this.socket.onmessage = (event) => { - const data = - typeof event.data === 'string' - ? event.data.split('|') - : JSON.stringify(event.data); - - if (data.length < 2) { - const json = JSON.parse(data[0]); - if (json.body) - this.logger.log( - `한국투자증권 웹소켓 연결: ${json.body.msg1}`, - json.header.tr_id, - ); - if (json.header.tr_id === 'PINGPONG') - this.socket.pong(JSON.stringify(json)); - return; - } - - const dataList = data[3].split('^'); - - const tradeData: TodayStockTradeHistoryDataDto = { - stck_shrn_iscd: dataList[0], - stck_cntg_hour: dataList[1], - stck_prpr: dataList[2], - prdy_vrss_sign: dataList[3], - cntg_vol: dataList[12], - prdy_ctrt: dataList[5], - }; - - const detailData: StockDetailSocketDataDto = { - stck_prpr: dataList[2], - prdy_vrss_sign: dataList[3], - prdy_vrss: dataList[4], - prdy_ctrt: dataList[5], - }; - - this.eventSubject.next({ - data: JSON.stringify({ - tradeData, - }), - }); - - this.socketGateway.sendStockTradeHistoryValueToClient( - `trade-history/${dataList[0]}`, - tradeData, - ); - - this.socketGateway.sendStockIndexValueToClient( - `detail/${dataList[0]}`, - detailData, - ); - }; - - this.socket.onclose = () => { - this.logger.warn(`한국투자증권 소켓 연결 종료`); - }; - } - - getTradeDataStream(targetStockCode: string): Observable { - return this.eventSubject.pipe( - filter((event: SseEvent) => { - const parsed = JSON.parse(event.data); - return parsed.tradeData.stck_shrn_iscd === targetStockCode; - }), - map((event: SseEvent) => event), - ); - } - - subscribeByCode(stockCode: string) { - this.registerCode(this.TR_ID, stockCode); - this.subscribedStocks.add(stockCode); - } - - unsubscribeByCode(stockCode: string) { - this.unregisterCode(this.TR_ID, stockCode); - this.subscribedStocks.delete(stockCode); - } - - registerCode(trId: string, trKey: string) { - this.socket.send( - JSON.stringify({ - header: { - approval_key: this.socketConnectionKey, - custtype: 'P', - tr_type: '1', - 'content-type': 'utf-8', - }, - body: { - input: { - tr_id: trId, - tr_key: trKey, - }, - }, - }), - ); - } - - unregisterCode(trId: string, trKey: string) { - this.socket.send( - JSON.stringify({ - header: { - approval_key: this.socketConnectionKey, - custtype: 'P', - tr_type: '2', - 'content-type': 'utf-8', - }, - body: { - input: { - tr_id: trId, - tr_key: trKey, - }, - }, - }), - ); - } - - async getSocketConnectionKey() { - if (this.socketConnectionKey) { - return this.socketConnectionKey; - } - - const response = await axios.post( - getFullTestURL('/oauth2/Approval'), - { - grant_type: 'client_credentials', - appkey: process.env.KOREA_INVESTMENT_TEST_APP_KEY, - secretkey: process.env.KOREA_INVESTMENT_TEST_APP_SECRET, - }, - ); - - this.socketConnectionKey = response.data.approval_key; - return this.socketConnectionKey; - } -} diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index b225642d..46c25762 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -1,18 +1,14 @@ -import { Observable } from 'rxjs'; -import { Controller, Get, Param, Sse } from '@nestjs/common'; +import { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; -import { StockTradeHistorySocketService } from './stock-trade-history-socket.service'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; -import { SseEvent } from './interface/sse-event'; @ApiTags('주식현재가 체결 조회 API') @Controller('/api/stocks/trade-history') export class StockTradeHistoryController { constructor( private readonly stockTradeHistoryService: StockTradeHistoryService, - private readonly stockTradeHistorySocketService: StockTradeHistorySocketService, ) {} @Get(':stockCode/today') @@ -30,12 +26,7 @@ export class StockTradeHistoryController { type: TodayStockTradeHistoryDataDto, }) getTodayStockTradeHistory(@Param('stockCode') stockCode: string) { - const data = - this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); - - this.stockTradeHistorySocketService.subscribeByCode(stockCode); - - return data; + return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } @Get(':stockCode/daily') @@ -56,35 +47,35 @@ export class StockTradeHistoryController { return this.stockTradeHistoryService.getDailyStockTradeHistory(stockCode); } - @Sse(':stockCode/today-sse') - @ApiOperation({ summary: '단일 주식 종목에 대한 실시간 체결 데이터 스트림' }) - @ApiParam({ - name: 'stockCode', - required: true, - description: - '종목 코드\n\n' + - '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', - }) - @ApiResponse({ - status: 200, - description: - '단일 주식 종목에 대한 주식현재가 체결값 실시간 데이터 조회 성공', - type: TodayStockTradeHistoryDataDto, - }) - streamTradeHistory(@Param('stockCode') stockCode: string) { - this.stockTradeHistorySocketService.subscribeByCode(stockCode); - - return new Observable((subscriber) => { - const subscription = this.stockTradeHistorySocketService - .getTradeDataStream(stockCode) - .subscribe(subscriber); - - return () => { - this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); - subscription.unsubscribe(); - }; - }); - } + // @Sse(':stockCode/today-sse') + // @ApiOperation({ summary: '단일 주식 종목에 대한 실시간 체결 데이터 스트림' }) + // @ApiParam({ + // name: 'stockCode', + // required: true, + // description: + // '종목 코드\n\n' + + // '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + // }) + // @ApiResponse({ + // status: 200, + // description: + // '단일 주식 종목에 대한 주식현재가 체결값 실시간 데이터 조회 성공', + // type: TodayStockTradeHistoryDataDto, + // }) + // streamTradeHistory(@Param('stockCode') stockCode: string) { + // this.stockTradeHistorySocketService.subscribeByCode(stockCode); + // + // return new Observable((subscriber) => { + // const subscription = this.stockTradeHistorySocketService + // .getTradeDataStream(stockCode) + // .subscribe(subscriber); + // + // return () => { + // this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); + // subscription.unsubscribe(); + // }; + // }); + // } @Get(':stockCode/unsubscribe') @ApiOperation({ summary: '페이지를 벗어날 때 구독을 취소하기 위한 API' }) @@ -100,6 +91,6 @@ export class StockTradeHistoryController { description: '구독 취소 성공', }) unsubscribeCode(@Param('stockCode') stockCode: string) { - this.stockTradeHistorySocketService.unsubscribeByCode(stockCode); + this.stockTradeHistoryService.unsubscribeCode(stockCode); } } diff --git a/BE/src/stock/trade/history/stock-trade-history.module.ts b/BE/src/stock/trade/history/stock-trade-history.module.ts index bcf067be..a3c55fd1 100644 --- a/BE/src/stock/trade/history/stock-trade-history.module.ts +++ b/BE/src/stock/trade/history/stock-trade-history.module.ts @@ -2,13 +2,12 @@ import { Module } from '@nestjs/common'; import { KoreaInvestmentModule } from '../../../common/koreaInvestment/korea-investment.module'; import { StockTradeHistoryController } from './stock-trade-history.controller'; import { StockTradeHistoryService } from './stock-trade-history.service'; -import { StockTradeHistorySocketService } from './stock-trade-history-socket.service'; import { SocketModule } from '../../../common/websocket/socket.module'; +import { StockSocketModule } from '../../../stockSocket/stock-socket.module'; @Module({ - imports: [KoreaInvestmentModule, SocketModule], + imports: [KoreaInvestmentModule, SocketModule, StockSocketModule], controllers: [StockTradeHistoryController], - providers: [StockTradeHistoryService, StockTradeHistorySocketService], - exports: [StockTradeHistorySocketService], + providers: [StockTradeHistoryService], }) export class StockTradeHistoryModule {} diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index 564ba4ca..9a3e2a7c 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -6,11 +6,13 @@ import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-d import { InquireDailyPriceApiResponse } from './interface/inquire-daily-price.interface'; import { DailyStockTradeHistoryOutputDto } from './dto/daily-stock-trade-history-ouput.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; +import { StockPriceSocketService } from '../../../stockSocket/stock-price-socket.service'; @Injectable() export class StockTradeHistoryService { constructor( private readonly koreaInvestmentDomainService: KoreaInvestmentDomainService, + private readonly stockPriceSocketService: StockPriceSocketService, ) {} /** @@ -33,6 +35,7 @@ export class StockTradeHistoryService { queryParams, ); + this.stockPriceSocketService.subscribeByCode(stockCode); return this.formatTodayStockTradeHistoryData(response.output); } @@ -107,4 +110,8 @@ export class StockTradeHistoryService { return historyData; }); } + + unsubscribeCode(stockCode: string) { + return this.stockPriceSocketService.unsubscribeByCode(stockCode); + } } From cfd5ffebf10870e8779b43c91e8112744c2b53fd Mon Sep 17 00:00:00 2001 From: Seo San Date: Tue, 26 Nov 2024 14:03:43 +0900 Subject: [PATCH 039/128] =?UTF-8?q?=E2=9C=A8=20feat:=20X=EC=B6=95=20?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80=20#192?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StocksDetail/Chart.tsx | 4 ++- FE/src/utils/chart/drawXAxis.ts | 42 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index 51f6aae7..51a7480f 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -27,7 +27,7 @@ const padding: Padding = { top: 20, right: 80, bottom: 10, - left: 20, + left: 40, }; type StocksDeatailChartProps = { @@ -240,6 +240,8 @@ export default function Chart({ code }: StocksDeatailChartProps) { chartXCanvas.width - padding.left - padding.right, chartXCanvas.height, padding, + mousePosition, + upperChartCanvas.height + lowerChartCanvas.height, ); if ( diff --git a/FE/src/utils/chart/drawXAxis.ts b/FE/src/utils/chart/drawXAxis.ts index 8d9943ff..774f00f6 100644 --- a/FE/src/utils/chart/drawXAxis.ts +++ b/FE/src/utils/chart/drawXAxis.ts @@ -1,6 +1,7 @@ import { Padding, StockChartUnit } from 'types.ts'; import { makeXLabels } from './makeLabels.ts'; import { formatTime } from '../formatTime.ts'; +import { MousePositionType } from '../../components/StocksDetail/Chart.tsx'; export const drawXAxis = ( ctx: CanvasRenderingContext2D, @@ -8,6 +9,8 @@ export const drawXAxis = ( width: number, height: number, padding: Padding, + mousePosition: MousePositionType, + parentHeight: number, ) => { const labels = makeXLabels(data); @@ -32,4 +35,43 @@ export const drawXAxis = ( ); } }); + + if ( + mousePosition.x > 0 && + mousePosition.x < width + padding.left + padding.right && + mousePosition.y > 0 && + mousePosition.y < parentHeight + ) { + const mouseX = mousePosition.x - padding.left; + const dataIndex = Math.floor((mouseX / width) * (data.length - 1)); + + if (dataIndex >= 0 && dataIndex < data.length) { + const boxPadding = 10; + const boxHeight = 30; + const mouseDate = data[dataIndex].stck_bsop_date; + const dateText = formatTime(mouseDate); + + ctx.font = '20px Arial '; + const textWidth = 100; + + const boxX = + padding.left + + (width * dataIndex) / (data.length - 1) - + textWidth / 2 + + 15; + const boxY = height / 2 - 20; + + // 박스 그리기 + ctx.fillStyle = '#2175F3'; + ctx.fillRect(boxX, boxY, textWidth + boxPadding * 2, boxHeight); + + // 텍스트 그리기 + ctx.fillStyle = '#FFF'; + ctx.fillText( + dateText, + padding.left + (width * dataIndex) / (data.length - 1) + 24, + boxY + boxHeight / 2 + 6, + ); + } + } }; From f1205446b645734a3767eb72dc9ce5f8e9d3fe8d Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 14:40:02 +0900 Subject: [PATCH 040/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=97=B0=EA=B2=B0=20=EC=B9=B4=EC=9A=B4=ED=8A=B8?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EC=97=B0=EA=B2=B0=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 4 ++-- BE/src/stockSocket/stock-price-socket.service.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/BE/src/main.ts b/BE/src/main.ts index 7f404bf8..febd76f1 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -13,10 +13,10 @@ async function bootstrap() { 'http://localhost:5173', 'http://juga.kro.kr:5173', 'http://juga.kro.kr:3000', - //개발 서버 + // 개발 서버 'http://223.130.151.42:5173', 'http://223.130.151.42:3000', - //배포 서버 + // 배포 서버 'http://175.45.204.158', 'http://175.45.204.158:3000', 'http://juga.kro.kr', diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts index 60695e13..f161c92b 100644 --- a/BE/src/stockSocket/stock-price-socket.service.ts +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -17,6 +17,7 @@ import { StockExecuteOrderRepository } from './stock-execute-order.repository'; @Injectable() export class StockPriceSocketService extends BaseStockSocketDomainService { private readonly logger = new Logger(); + private connection: { [key: string]: number } = {}; constructor( protected readonly socketGateway: SocketGateway, @@ -71,10 +72,16 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { subscribeByCode(trKey: string) { this.baseSocketDomainService.registerCode(this.TR_ID, trKey); + + if (this.connection[trKey]) return this.connection[trKey]++; + return (this.connection[trKey] = 1); } unsubscribeByCode(trKey: string) { - this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); + if (!this.connection[trKey]) return; + if (this.connection[trKey] > 1) return this.connection[trKey]--; + delete this.connection[trKey]; + return this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); } private async checkExecutableOrder(stockCode: string, value) { @@ -102,6 +109,7 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { ); if ( + buyOrders.length + sellOrders.length > 0 && !(await this.stockExecuteOrderRepository.existsBy({ stock_code: stockCode, status: StatusType.PENDING, From ab004c2f4647702acf14e50ab4b1bf90281f98b8 Mon Sep 17 00:00:00 2001 From: Seo San Date: Tue, 26 Nov 2024 14:51:03 +0900 Subject: [PATCH 041/128] =?UTF-8?q?=E2=9C=A8=20feat:=20Y=EC=B6=95=20?= =?UTF-8?q?=EA=B0=92=20=EC=83=9D=EC=84=B1=20#192?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StocksDetail/Chart.tsx | 7 +++++ FE/src/utils/chart/drawLowerYAxis.ts | 40 ++++++++++++++++++++++++ FE/src/utils/chart/drawUpperYAxis.ts | 37 ++++++++++++++++++++++ FE/src/utils/chart/drawXAxis.ts | 12 +++---- 4 files changed, 89 insertions(+), 7 deletions(-) diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index 51a7480f..832aed4c 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -223,6 +223,9 @@ export default function Chart({ code }: StocksDeatailChartProps) { upperLabelNum, padding, 0.1, + mousePosition, + upperChartCanvas.width, + upperChartCanvas.height, ); drawLowerYAxis( @@ -232,6 +235,10 @@ export default function Chart({ code }: StocksDeatailChartProps) { lowerChartYCanvas.height - padding.top - padding.bottom, lowerLabelNum, padding, + mousePosition, + lowerChartCanvas.width, + lowerChartCanvas.height, + upperChartCanvas.height, ); drawXAxis( diff --git a/FE/src/utils/chart/drawLowerYAxis.ts b/FE/src/utils/chart/drawLowerYAxis.ts index f6c68ed3..fa09f53f 100644 --- a/FE/src/utils/chart/drawLowerYAxis.ts +++ b/FE/src/utils/chart/drawLowerYAxis.ts @@ -1,5 +1,6 @@ import { Padding, StockChartUnit } from '../../types.ts'; import { makeYLabels } from './makeLabels.ts'; +import { MousePositionType } from '../../components/StocksDetail/Chart.tsx'; export const drawLowerYAxis = ( ctx: CanvasRenderingContext2D, @@ -8,6 +9,10 @@ export const drawLowerYAxis = ( height: number, labelsNum: number, padding: Padding, + mousePosition: MousePositionType, + lowerChartWidth: number, + lowerChartHeight: number, + upperChartHeight: number, ) => { ctx.clearRect( 0, @@ -32,6 +37,41 @@ export const drawLowerYAxis = ( const formattedValue = formatNumber(label); ctx.fillText(formattedValue, width / 2 + padding.left, yPos + padding.top); }); + + if ( + mousePosition.x > padding.left && + mousePosition.x < lowerChartWidth + padding.left && + mousePosition.y > upperChartHeight && + mousePosition.y < upperChartHeight + lowerChartHeight + ) { + const relativeY = + mousePosition.y - padding.top - padding.bottom - upperChartHeight; + + const valueRatio = 1 - relativeY / height; + const mouseValue = yMin + valueRatio * (yMax - yMin); + + const boxPadding = 10; + const boxHeight = 30; + const valueText = formatNumber(Math.round(mouseValue)); + + ctx.font = '24px Arial'; + const textWidth = ctx.measureText(valueText).width; + + ctx.fillStyle = '#2175F3'; + const boxX = width / 2 + padding.left - 12; + const boxY = + mousePosition.y - upperChartHeight - padding.bottom - boxHeight / 2; + + ctx.fillRect(boxX, boxY, textWidth + boxPadding * 2, boxHeight); + + ctx.fillStyle = '#FFF'; + ctx.textAlign = 'center'; + ctx.fillText( + valueText, + boxX + (textWidth + boxPadding * 2) / 2, + boxY + boxHeight / 2 + 2, + ); + } }; const formatNumber = (value: number) => { diff --git a/FE/src/utils/chart/drawUpperYAxis.ts b/FE/src/utils/chart/drawUpperYAxis.ts index d7a1df78..5eeb8e93 100644 --- a/FE/src/utils/chart/drawUpperYAxis.ts +++ b/FE/src/utils/chart/drawUpperYAxis.ts @@ -1,5 +1,6 @@ import { Padding, StockChartUnit } from '../../types.ts'; import { makeYLabels } from './makeLabels.ts'; +import { MousePositionType } from '../../components/StocksDetail/Chart.tsx'; export const drawUpperYAxis = ( ctx: CanvasRenderingContext2D, @@ -9,6 +10,9 @@ export const drawUpperYAxis = ( labelsNum: number, padding: Padding, weight: number = 0, + mousePosition: MousePositionType, + upperChartWidth: number, + upperChartHeight: number, ) => { const values = data .map((d) => [+d.stck_hgpr, +d.stck_lwpr, +d.stck_clpr, +d.stck_oprc]) @@ -35,4 +39,37 @@ export const drawUpperYAxis = ( const formattedValue = label.toLocaleString(); ctx.fillText(formattedValue, width / 2 + padding.left, yPos + padding.top); }); + + if ( + mousePosition.x > padding.left && + mousePosition.x < upperChartWidth + padding.left && + mousePosition.y > padding.top && + mousePosition.y < upperChartHeight + ) { + const relativeY = mousePosition.y - padding.top; + + const valueRatio = 1 - relativeY / height; + const mouseValue = yMin + valueRatio * (yMax - yMin); + + const boxPadding = 10; + const boxHeight = 30; + const valueText = Math.round(mouseValue).toLocaleString(); + + ctx.font = '24px Arial'; + const textWidth = ctx.measureText(valueText).width; + + ctx.fillStyle = '#2175F3'; + const boxX = width / 2 + padding.left - 12; + const boxY = mousePosition.y - boxHeight / 2; + + ctx.fillRect(boxX, boxY, textWidth + boxPadding * 2, boxHeight); + + ctx.fillStyle = '#FFF'; + ctx.textAlign = 'center'; + ctx.fillText( + valueText, + boxX + (textWidth + boxPadding * 2) / 2, + boxY + boxHeight / 2 + 2, + ); + } }; diff --git a/FE/src/utils/chart/drawXAxis.ts b/FE/src/utils/chart/drawXAxis.ts index 774f00f6..1ec02bf9 100644 --- a/FE/src/utils/chart/drawXAxis.ts +++ b/FE/src/utils/chart/drawXAxis.ts @@ -21,7 +21,7 @@ export const drawXAxis = ( height + padding.top + padding.bottom, ); - ctx.font = '20px Arial'; + ctx.font = '22px Arial'; ctx.textAlign = 'center'; ctx.fillStyle = '#000'; @@ -39,7 +39,7 @@ export const drawXAxis = ( if ( mousePosition.x > 0 && mousePosition.x < width + padding.left + padding.right && - mousePosition.y > 0 && + mousePosition.y > padding.top && mousePosition.y < parentHeight ) { const mouseX = mousePosition.x - padding.left; @@ -51,8 +51,8 @@ export const drawXAxis = ( const mouseDate = data[dataIndex].stck_bsop_date; const dateText = formatTime(mouseDate); - ctx.font = '20px Arial '; - const textWidth = 100; + ctx.font = '22px Arial '; + const textWidth = ctx.measureText(dateText).width; const boxX = padding.left + @@ -61,16 +61,14 @@ export const drawXAxis = ( 15; const boxY = height / 2 - 20; - // 박스 그리기 ctx.fillStyle = '#2175F3'; ctx.fillRect(boxX, boxY, textWidth + boxPadding * 2, boxHeight); - // 텍스트 그리기 ctx.fillStyle = '#FFF'; ctx.fillText( dateText, padding.left + (width * dataIndex) / (data.length - 1) + 24, - boxY + boxHeight / 2 + 6, + boxY + boxHeight / 2 + 8, ); } } From 63c7bc2e984b58e2152e5467f4906d3473e46534 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 15:03:10 +0900 Subject: [PATCH 042/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20lint=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.ts | 2 +- BE/src/stock/order/stock-order.repository.ts | 4 +--- BE/src/stockSocket/stock-price-socket.service.ts | 14 ++++++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index edf1d064..5cdd0a05 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -98,7 +98,7 @@ export class RankingService { return { topRank: parsedTopRank, - userRank: userRank, + userRank, }; } diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 84c453ee..c79abd48 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -1,10 +1,8 @@ import { DataSource, Repository } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Order } from './stock-order.entity'; import { StatusType } from './enum/status-type'; -import { Asset } from '../../asset/asset.entity'; -import { UserStock } from '../../asset/user-stock.entity'; import { StockOrderRawInterface } from './interface/stock-order-raw.interface'; @Injectable() diff --git a/BE/src/stockSocket/stock-price-socket.service.ts b/BE/src/stockSocket/stock-price-socket.service.ts index f161c92b..19348a6b 100644 --- a/BE/src/stockSocket/stock-price-socket.service.ts +++ b/BE/src/stockSocket/stock-price-socket.service.ts @@ -73,15 +73,21 @@ export class StockPriceSocketService extends BaseStockSocketDomainService { subscribeByCode(trKey: string) { this.baseSocketDomainService.registerCode(this.TR_ID, trKey); - if (this.connection[trKey]) return this.connection[trKey]++; - return (this.connection[trKey] = 1); + if (this.connection[trKey]) { + this.connection[trKey] += 1; + return; + } + this.connection[trKey] = 1; } unsubscribeByCode(trKey: string) { if (!this.connection[trKey]) return; - if (this.connection[trKey] > 1) return this.connection[trKey]--; + if (this.connection[trKey] > 1) { + this.connection[trKey] -= 1; + return; + } delete this.connection[trKey]; - return this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); + this.baseSocketDomainService.unregisterCode(this.TR_ID, trKey); } private async checkExecutableOrder(stockCode: string, value) { From f29dda1d228e13fc7e22051fa94259220dab2139 Mon Sep 17 00:00:00 2001 From: dongree Date: Tue, 26 Nov 2024 15:22:40 +0900 Subject: [PATCH 043/128] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20header=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=9B=B9=EC=86=8C=EC=BC=93=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StocksDetail/Header.tsx | 42 +++++++++++++++++++---- FE/src/service/stocks.ts | 9 +++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/FE/src/components/StocksDetail/Header.tsx b/FE/src/components/StocksDetail/Header.tsx index 9f3582e3..173daf23 100644 --- a/FE/src/components/StocksDetail/Header.tsx +++ b/FE/src/components/StocksDetail/Header.tsx @@ -1,4 +1,8 @@ +import { useEffect, useState } from 'react'; +import { unsbscribe } from 'service/stocks'; import { StockDetailType } from 'types'; +import { stringToLocaleString } from 'utils/common'; +import { socket } from 'utils/socket'; type StocksDeatailHeaderProps = { code: string; @@ -16,22 +20,48 @@ export default function Header({ code, data }: StocksDeatailHeaderProps) { per, } = data; + const [currPrice, setCurrPrice] = useState(stck_prpr); + const [currPrdyVrssSign, setCurrPrdyVrssSign] = useState(prdy_vrss_sign); + const [currPrdyVrss, setCurrPrdyVrss] = useState(prdy_vrss); + const [currPrdyRate, setCurrPrdyRate] = useState(prdy_ctrt); + + useEffect(() => { + const handleSocketData = (data: { + stck_prpr: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + }) => { + const { stck_prpr, prdy_vrss, prdy_vrss_sign, prdy_ctrt } = data; + setCurrPrice(stck_prpr); + setCurrPrdyVrss(prdy_vrss); + setCurrPrdyVrssSign(prdy_vrss_sign); + setCurrPrdyRate(prdy_ctrt); + }; + + socket.on(`detail/${code}`, handleSocketData); + + return () => { + unsbscribe(code); + }; + }, [code]); + const stockInfo: { label: string; value: string }[] = [ { label: '시총', value: `${Number(hts_avls).toLocaleString()}억원` }, { label: 'PER', value: `${per}배` }, ]; const colorStyleBySign = - prdy_vrss_sign === '3' + currPrdyVrssSign === '3' ? '' - : prdy_vrss_sign < '3' + : currPrdyVrssSign < '3' ? 'text-juga-red-60' : 'text-juga-blue-40'; - const percentAbsolute = Math.abs(Number(prdy_ctrt)).toFixed(2); + const percentAbsolute = Math.abs(Number(currPrdyRate)).toFixed(2); const plusOrMinus = - prdy_vrss_sign === '3' ? '' : prdy_vrss_sign < '3' ? '+' : '-'; + currPrdyVrssSign === '3' ? '' : currPrdyVrssSign < '3' ? '+' : '-'; return (
@@ -41,11 +71,11 @@ export default function Header({ code, data }: StocksDeatailHeaderProps) {

{code}

-

{Number(stck_prpr).toLocaleString()}원

+

{stringToLocaleString(currPrice)}원

어제보다

{plusOrMinus} - {Math.abs(Number(prdy_vrss)).toLocaleString()}원 ({plusOrMinus} + {Math.abs(Number(currPrdyVrss)).toLocaleString()}원 ({plusOrMinus} {percentAbsolute}%)

diff --git a/FE/src/service/stocks.ts b/FE/src/service/stocks.ts index 9f5d4bb4..fae307b9 100644 --- a/FE/src/service/stocks.ts +++ b/FE/src/service/stocks.ts @@ -22,3 +22,12 @@ export async function getStocksChartDataByCode( }), }).then((res) => res.json()); } + +export async function unsbscribe(code: string) { + return fetch( + `${import.meta.env.VITE_API_URL}/stocks/trade-history/${code}/unsubscribe`, + { + headers: { 'Content-Type': 'application/json' }, + }, + ); +} From 096f7f1268cb43c30b093abb99b73645408f4218 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 26 Nov 2024 15:56:04 +0900 Subject: [PATCH 044/128] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20cheerio?= =?UTF-8?q?=20=EC=84=A4=EC=B9=98#181?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/package-lock.json | 327 +++++++++++++++++++++++++++++++++++++++++++ BE/package.json | 1 + 2 files changed, 328 insertions(+) diff --git a/BE/package-lock.json b/BE/package-lock.json index b443ce76..0d943ed6 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -24,6 +24,7 @@ "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", + "cheerio": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", @@ -3201,6 +3202,12 @@ "version": "2.0.0", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/boom": { "version": "2.10.1", "license": "BSD-3-Clause", @@ -3412,6 +3419,85 @@ "dev": true, "license": "MIT" }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "dev": true, @@ -3971,6 +4057,34 @@ "urix": "^0.1.0" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css/node_modules/source-map": { "version": "0.6.1", "license": "BSD-3-Clause", @@ -4268,6 +4382,73 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.5", "license": "BSD-2-Clause", @@ -4409,6 +4590,31 @@ "node": ">=10.13.0" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/engine.io": { "version": "6.6.2", "license": "MIT", @@ -6210,6 +6416,37 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "license": "MIT", @@ -8388,6 +8625,18 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -8673,6 +8922,42 @@ "version": "6.0.1", "license": "MIT" }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", @@ -11088,6 +11373,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", + "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "license": "MIT" @@ -11375,6 +11669,39 @@ "node": ">=4.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "license": "MIT", diff --git a/BE/package.json b/BE/package.json index 9bff210b..cd3e7edd 100644 --- a/BE/package.json +++ b/BE/package.json @@ -35,6 +35,7 @@ "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", + "cheerio": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", From 2385a990cc2b211f63cbf53f87fc3dd1366208fd Mon Sep 17 00:00:00 2001 From: Seo San Date: Tue, 26 Nov 2024 15:56:33 +0900 Subject: [PATCH 045/128] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=20=EC=9D=B4=EB=8F=99=EC=9D=84=20req?= =?UTF-8?q?uestAnimationFrame=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=88=98=EC=A0=95=20#192=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=97=90=EC=84=9C=20requestAnimationFrame?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=94=84=EB=A0=88=EC=9E=84=EB=A7=88=EB=8B=A4=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=EC=9D=84=20=EA=B0=90=EC=A7=80=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=ED=95=98=EC=97=AC=20=EB=B9=88=EB=B2=88=ED=95=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=9D=84=20=EC=A4=84=EC=9D=B4=EB=8F=84?= =?UTF-8?q?=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 --- FE/src/components/StocksDetail/Chart.tsx | 23 ++++++++++++++++++----- FE/src/utils/chart/drawLowerYAxis.ts | 2 +- FE/src/utils/chart/drawUpperYAxis.ts | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index 832aed4c..aa406165 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -46,7 +46,8 @@ export default function Chart({ code }: StocksDeatailChartProps) { const upperChartY = useRef(null); const lowerChartY = useRef(null); const chartX = useRef(null); - + // RAF 관리를 위한 ref + const rafRef = useRef(); const [timeCategory, setTimeCategory] = useState('D'); const [charSizeConfig, setChartSizeConfig] = useState({ upperHeight: 0.5, @@ -117,10 +118,19 @@ export default function Chart({ code }: StocksDeatailChartProps) { const getCanvasMousePosition = (e: MouseEvent) => { if (!containerRef.current) return; - const rect = containerRef.current.getBoundingClientRect(); - setMousePosition({ - x: (e.clientX - rect.left) * 2, - y: (e.clientY - rect.top) * 2, + + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + + rafRef.current = requestAnimationFrame(() => { + const rect = containerRef.current?.getBoundingClientRect(); + if (!rect) return; + + setMousePosition({ + x: (e.clientX - rect.left) * 2, + y: (e.clientY - rect.top) * 2, + }); }); }; @@ -133,6 +143,9 @@ export default function Chart({ code }: StocksDeatailChartProps) { return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } }; }, [isDragging, handleMouseDown, handleMouseUp, handleMouseMove]); const setCanvasSize = useCallback( diff --git a/FE/src/utils/chart/drawLowerYAxis.ts b/FE/src/utils/chart/drawLowerYAxis.ts index fa09f53f..65fcf9b4 100644 --- a/FE/src/utils/chart/drawLowerYAxis.ts +++ b/FE/src/utils/chart/drawLowerYAxis.ts @@ -40,7 +40,7 @@ export const drawLowerYAxis = ( if ( mousePosition.x > padding.left && - mousePosition.x < lowerChartWidth + padding.left && + mousePosition.x < lowerChartWidth && mousePosition.y > upperChartHeight && mousePosition.y < upperChartHeight + lowerChartHeight ) { diff --git a/FE/src/utils/chart/drawUpperYAxis.ts b/FE/src/utils/chart/drawUpperYAxis.ts index 5eeb8e93..4eb541d7 100644 --- a/FE/src/utils/chart/drawUpperYAxis.ts +++ b/FE/src/utils/chart/drawUpperYAxis.ts @@ -42,7 +42,7 @@ export const drawUpperYAxis = ( if ( mousePosition.x > padding.left && - mousePosition.x < upperChartWidth + padding.left && + mousePosition.x < upperChartWidth && mousePosition.y > padding.top && mousePosition.y < upperChartHeight ) { From d6e28411b50ce5eb72129a7af8ba8ac20e08da01 Mon Sep 17 00:00:00 2001 From: dongree Date: Tue, 26 Nov 2024 16:35:38 +0900 Subject: [PATCH 046/128] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20buy,?= =?UTF-8?q?=20sell=20=EC=9A=94=EC=B2=AD=20custom=20hooks=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 --- .../TradeSection/TradeAlertModal.tsx | 11 +++++----- FE/src/hooks/useOrder.ts | 21 +++++++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx index 015f236d..788b70eb 100644 --- a/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx +++ b/FE/src/components/StocksDetail/TradeSection/TradeAlertModal.tsx @@ -1,5 +1,5 @@ import Overay from 'components/ModalOveray'; -import { orderBuyStock, orderSellStock } from 'service/orders'; +import useOrders from 'hooks/useOrder'; import useTradeAlertModalStore from 'store/tradeAlertModalStore'; import { getTradeCommision } from 'utils/common'; @@ -19,6 +19,7 @@ export default function TradeAlertModal({ type, }: TradeAlertModalProps) { const { toggleModal } = useTradeAlertModalStore(); + const { orderBuy, orderSell } = useOrders(); const totalPrice = +price * count; @@ -26,11 +27,11 @@ export default function TradeAlertModal({ const handleTrade = async () => { if (type === 'BUY') { - const res = await orderBuyStock(code, +price, count); - if (res.ok) toggleModal(); + orderBuy.mutate({ code, price: +price, count }); + toggleModal(); } else { - const res = await orderSellStock(code, +price, count); - if (res.ok) toggleModal(); + orderSell.mutate({ code, price: +price, count }); + toggleModal(); } }; diff --git a/FE/src/hooks/useOrder.ts b/FE/src/hooks/useOrder.ts index 0c23d661..501c0b45 100644 --- a/FE/src/hooks/useOrder.ts +++ b/FE/src/hooks/useOrder.ts @@ -1,5 +1,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { deleteOrder, getOrders } from 'service/orders'; +import { + deleteOrder, + getOrders, + orderBuyStock, + orderSellStock, +} from 'service/orders'; export default function useOrders() { const queryClient = useQueryClient(); @@ -10,5 +15,17 @@ export default function useOrders() { onSuccess: () => queryClient.invalidateQueries(['account', 'order']), }); - return { orderQuery, removeOrder }; + const orderBuy = useMutation( + ({ code, price, count }: { code: string; price: number; count: number }) => + orderBuyStock(code, price, count), + { onSuccess: () => queryClient.invalidateQueries(['detail', 'cash']) }, + ); + + const orderSell = useMutation( + ({ code, price, count }: { code: string; price: number; count: number }) => + orderSellStock(code, price, count), + { onSuccess: () => queryClient.invalidateQueries(['detail', 'cash']) }, + ); + + return { orderQuery, removeOrder, orderBuy, orderSell }; } From 47b66a0b890888a0973b49a0ec566a2534af8ad3 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 16:36:59 +0900 Subject: [PATCH 047/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=A6=90?= =?UTF-8?q?=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EC=97=AC=EB=B6=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=EC=99=80=20detail=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EC=83=81=EB=8B=A8=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/dto/stock-detail-response.dto.ts | 3 ++ .../stock/detail/stock-detail.controller.ts | 29 +++++++++++++++++-- BE/src/stock/detail/stock-detail.module.ts | 1 + .../stock/detail/stock-detail.repository.ts | 11 ++++++- BE/src/stock/detail/stock-detail.service.ts | 8 +++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/BE/src/stock/detail/dto/stock-detail-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts index 6c7d1aa6..65c412e5 100644 --- a/BE/src/stock/detail/dto/stock-detail-response.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-response.dto.ts @@ -30,4 +30,7 @@ export class InquirePriceResponseDto { @ApiProperty({ description: '주식 하한가' }) stck_llam: string; + + @ApiProperty({ description: '즐겨찾기 여부 (미인증시 false)' }) + is_bookmarked: boolean; } diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 5df8d6c7..099a5238 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,15 +1,26 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { + Body, + Controller, + Get, + Param, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { + ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags, } from '@nestjs/swagger'; +import { Request } from 'express'; import { StockDetailService } from './stock-detail.service'; import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; import { StockDetailChartRequestDto } from './dto/stock-detail-chart-request.dto'; import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; +import { OptionalAuthGuard } from '../../auth/optional-auth-guard'; @ApiTags('특정 주식 종목에 대한 detail 페이지 조회 API') @Controller('/api/stocks/detail') @@ -17,6 +28,8 @@ export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} @Get(':stockCode') + @ApiBearerAuth() + @UseGuards(OptionalAuthGuard) @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) @ApiParam({ name: 'stockCode', @@ -30,8 +43,18 @@ export class StockDetailController { description: '단일 주식 종목 기본값 조회 성공', type: InquirePriceResponseDto, }) - getStockDetail(@Param('stockCode') stockCode: string) { - return this.stockDetailService.getInquirePrice(stockCode); + async getStockDetail( + @Req() request: Request, + @Param('stockCode') stockCode: string, + ) { + const stockData = await this.stockDetailService.getInquirePrice(stockCode); + if (!request.user) return stockData; + + stockData.is_bookmarked = await this.stockDetailService.getBookmarkActive( + parseInt(request.user.userId, 10), + stockCode, + ); + return stockData; } @Post(':stockCode') diff --git a/BE/src/stock/detail/stock-detail.module.ts b/BE/src/stock/detail/stock-detail.module.ts index 67899209..3ed5ad0f 100644 --- a/BE/src/stock/detail/stock-detail.module.ts +++ b/BE/src/stock/detail/stock-detail.module.ts @@ -5,6 +5,7 @@ import { StockDetailController } from './stock-detail.controller'; import { StockDetailService } from './stock-detail.service'; import { StockDetailRepository } from './stock-detail.repository'; import { Stocks } from './stock-detail.entity'; +import { StockBookmarkModule } from '../bookmark/stock-bookmark.module'; @Module({ imports: [KoreaInvestmentModule, TypeOrmModule.forFeature([Stocks])], diff --git a/BE/src/stock/detail/stock-detail.repository.ts b/BE/src/stock/detail/stock-detail.repository.ts index d9143719..ecc400d7 100644 --- a/BE/src/stock/detail/stock-detail.repository.ts +++ b/BE/src/stock/detail/stock-detail.repository.ts @@ -2,14 +2,23 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { Injectable } from '@nestjs/common'; import { DataSource, Repository } from 'typeorm'; import { Stocks } from './stock-detail.entity'; +import { Bookmark } from '../bookmark/stock-bookmark.entity'; @Injectable() export class StockDetailRepository extends Repository { - constructor(@InjectDataSource() dataSource: DataSource) { + constructor(@InjectDataSource() private readonly dataSource: DataSource) { super(Stocks, dataSource.createEntityManager()); } async findOneByCode(code: string) { return this.findOne({ where: { code } }); } + + async existsBookmarkByUserIdAndStockCode(userId: number, stockCode: string) { + const queryRunner = this.dataSource.createQueryRunner(); + return queryRunner.manager.existsBy(Bookmark, { + user_id: userId, + stock_code: stockCode, + }); + } } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index b3e537ed..be2b8a34 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -63,6 +63,7 @@ export class StockDetailService { per: stock.per, stck_mxpr: stock.stck_mxpr, stck_llam: stock.stck_llam, + is_bookmarked: false, }; } @@ -101,6 +102,13 @@ export class StockDetailService { return this.formatStockInquirePriceData(response).slice().reverse(); } + getBookmarkActive(userId: number, stockCode: string) { + return this.stockDetailRepository.existsBookmarkByUserIdAndStockCode( + userId, + stockCode, + ); + } + /** * @private API에서 받은 국내주식기간별시세(일/주/월/년) 데이터를 필요한 정보로 정제하는 함수 * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 From e0073fac24724692e67c5b9099ec43bf649622bf Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 26 Nov 2024 16:38:10 +0900 Subject: [PATCH 048/128] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bookmark/stock-bookmark.controller.ts | 19 ------------------- .../stock/bookmark/stock-bookmark.service.ts | 9 --------- 2 files changed, 28 deletions(-) diff --git a/BE/src/stock/bookmark/stock-bookmark.controller.ts b/BE/src/stock/bookmark/stock-bookmark.controller.ts index 886f5e79..f81fa4fc 100644 --- a/BE/src/stock/bookmark/stock-bookmark.controller.ts +++ b/BE/src/stock/bookmark/stock-bookmark.controller.ts @@ -73,23 +73,4 @@ export class StockBookmarkController { parseInt(request.user.userId, 10), ); } - - @Get('/:stockCode') - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: '특정 종목에 대한 즐겨찾기 등록 여부 조회 API' }) - @ApiResponse({ - status: 200, - description: '즐겨찾기 등록 여부 조회 성공', - example: { is_bookmarked: true }, - }) - async getBookmarkActive( - @Req() request: Request, - @Param('stockCode') stockCode: string, - ) { - return this.stockBookmarkService.getBookmarkActive( - parseInt(request.user.userId, 10), - stockCode, - ); - } } diff --git a/BE/src/stock/bookmark/stock-bookmark.service.ts b/BE/src/stock/bookmark/stock-bookmark.service.ts index 17710c39..623f0358 100644 --- a/BE/src/stock/bookmark/stock-bookmark.service.ts +++ b/BE/src/stock/bookmark/stock-bookmark.service.ts @@ -62,13 +62,4 @@ export class StockBookmarkService { }), ); } - - async getBookmarkActive(userId, stockCode) { - return { - is_bookmarked: await this.stockBookmarkRepository.existsBy({ - user_id: userId, - stock_code: stockCode, - }), - }; - } } From bd486965250985f9194ad97a0f73f8e1045e4729 Mon Sep 17 00:00:00 2001 From: dongree Date: Tue, 26 Nov 2024 16:40:50 +0900 Subject: [PATCH 049/128] =?UTF-8?q?=E2=9E=95=20add:=20react=20query=20stal?= =?UTF-8?q?e=20time=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/Mypage/Account.tsx | 6 ++++-- FE/src/components/StocksDetail/Chart.tsx | 3 ++- FE/src/components/StocksDetail/TradeSection/BuySection.tsx | 6 ++++-- FE/src/components/StocksDetail/TradeSection/SellSection.tsx | 1 + FE/src/hooks/useOrder.ts | 4 +++- FE/src/page/StocksDetail.tsx | 2 +- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/FE/src/components/Mypage/Account.tsx b/FE/src/components/Mypage/Account.tsx index 3b9e5f09..9cb3d134 100644 --- a/FE/src/components/Mypage/Account.tsx +++ b/FE/src/components/Mypage/Account.tsx @@ -4,8 +4,10 @@ import MyStocksList from './MyStocksList'; import { getAssets } from 'service/assets'; export default function Account() { - const { data, isLoading, isError } = useQuery(['account', 'assets'], () => - getAssets(), + const { data, isLoading, isError } = useQuery( + ['account', 'assets'], + () => getAssets(), + { staleTime: 1000 }, ); if (isLoading) return
loading
; diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index 51f6aae7..191529f1 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -66,6 +66,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { const { data, isLoading } = useQuery( ['stocksChartData', code, timeCategory], () => getStocksChartDataByCode(code, timeCategory), + { staleTime: 1000 }, ); const handleMouseDown = useCallback((e: MouseEvent) => { @@ -336,7 +337,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { return (
-
+

차트