diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index e519f05b..3a13ebc0 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -6,7 +6,7 @@ on: - main - dev - dev-be - - feature-be-#51 + - feature-be-#105 jobs: build_and_deploy: runs-on: ubuntu-latest diff --git a/nginx.conf b/nginx.conf index 6d03ad5c..57e7f456 100644 --- a/nginx.conf +++ b/nginx.conf @@ -35,6 +35,9 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + # Origin 헤더 유지 + proxy_set_header Origin $http_origin; + # CORS 설정 추가 add_header 'Access-Control-Allow-Origin' 'https://www.corinee.site' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; diff --git a/packages/client/src/Router.tsx b/packages/client/src/Router.tsx index 0e3bc8b7..848798ae 100644 --- a/packages/client/src/Router.tsx +++ b/packages/client/src/Router.tsx @@ -5,18 +5,21 @@ import Account from '@/pages/account/Account'; import Trade from '@/pages/trade/Trade'; import NotFound from '@/pages/not-found/NotFound'; import Redricet from '@/pages/auth/Redirect'; +import { Suspense } from 'react'; function Router() { return ( - - }> - } /> - } /> - } /> - - } /> - } /> - + + + }> + } /> + } /> + } /> + + } /> + } /> + + ); } diff --git a/packages/client/src/api/interceptors.ts b/packages/client/src/api/interceptors.ts index 09ebd396..44977eb3 100644 --- a/packages/client/src/api/interceptors.ts +++ b/packages/client/src/api/interceptors.ts @@ -1,6 +1,6 @@ import { authInstance, instance } from '@/api/instance'; import { Login } from '@/types/auth'; -import { getCookie, removeCookie, setCookie } from '@/utility/cookies'; +import { getCookie, removeCookie, setCookie } from '@/utility/storage/cookies'; import { AxiosResponse } from 'axios'; authInstance.interceptors.request.use( diff --git a/packages/client/src/components/Header.tsx b/packages/client/src/components/Header.tsx index b5c8e43e..55e16b68 100644 --- a/packages/client/src/components/Header.tsx +++ b/packages/client/src/components/Header.tsx @@ -1,6 +1,6 @@ import { useAuth } from '@/hooks/auth/useAuth'; import { Button, Navbar } from '@material-tailwind/react'; -import { Link } from 'react-router-dom'; +import { Link, NavLink } from 'react-router-dom'; import { useAuthStore } from '@/store/authStore'; import { useToast } from '@/hooks/ui/useToast'; import logoImage from '@asset/logo/corineeLogo.png'; @@ -22,7 +22,7 @@ function Header() { <>
@@ -32,12 +32,22 @@ function Header() {
- + + `${isActive ? 'text-black' : 'text-gray-600'} hover:text-black` + } + > 홈 - - + + + `${isActive ? 'text-black' : 'text-gray-600'} hover:text-black` + } + > 내 계좌 - +
{isAuthenticated ? ( @@ -63,11 +73,7 @@ function Header() { kakao_image
)} diff --git a/packages/client/src/components/sidebar/RecentlyViewed.tsx b/packages/client/src/components/sidebar/RecentlyViewed.tsx index 1beb68fc..e05a438f 100644 --- a/packages/client/src/components/sidebar/RecentlyViewed.tsx +++ b/packages/client/src/components/sidebar/RecentlyViewed.tsx @@ -3,7 +3,7 @@ import { useSSETicker } from '@/hooks/SSE/useSSETicker'; import { formatData } from '@/utility/format/formatSSEData'; import useRecentlyMarketStore from '@/store/recentlyViewed'; import { useRecentlyMarketList } from '@/hooks/market/useRecentlyMarket'; -import { convertToQueryString } from '@/utility/queryString'; +import { convertToQueryString } from '@/utility/api/queryString'; import { SidebarMarketData } from '@/types/market'; function RecentlyViewed() { diff --git a/packages/client/src/components/sidebar/Sidebar.tsx b/packages/client/src/components/sidebar/Sidebar.tsx index 29e8404a..fd12a9d9 100644 --- a/packages/client/src/components/sidebar/Sidebar.tsx +++ b/packages/client/src/components/sidebar/Sidebar.tsx @@ -45,9 +45,9 @@ function Sidebar() { ]; return ( -
+
-
+
{SIDEBAR_BUTTONS.map((button) => ( (null); + const chartInstanceRef = useRef(null); + const seriesRef = useRef | null>(null); + + useEffect(() => { + if (!chartRef.current) return; + chartInstanceRef.current = initializeChart(chartRef.current, chartConfig); + seriesRef.current = setupCandlestickSeries( + chartInstanceRef.current, + [], + chartConfig, + ); + const resizeObserver = new ResizeObserver(() => { + handleResize(chartRef, chartInstanceRef); + }); + + if (chartRef.current.parentElement) { + resizeObserver.observe(chartRef.current.parentElement); + } + + return () => { + if (chartInstanceRef.current) { + resizeObserver.disconnect(); + chartInstanceRef.current.remove(); + } + }; + }, []); + + return { chartRef, chartInstanceRef, seriesRef }; +} diff --git a/packages/client/src/hooks/chart/usePeriodChart.ts b/packages/client/src/hooks/chart/usePeriodChart.ts new file mode 100644 index 00000000..398c5f92 --- /dev/null +++ b/packages/client/src/hooks/chart/usePeriodChart.ts @@ -0,0 +1,40 @@ +import { getCandleByPeriod } from '@/api/market'; +import { Candle, CandlePeriod, InfiniteCandle } from '@/types/chart'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; + +export function usePeriodChart( + market: string, + period: CandlePeriod, + minute?: number, +) { + const { data, fetchNextPage, hasNextPage, refetch } = + useSuspenseInfiniteQuery< + Candle[], + Error, + InfiniteCandle, + [string, string, CandlePeriod, number?], + string | undefined + >({ + queryKey: ['candles', market, period, minute], + queryFn: ({ pageParam }) => { + return getCandleByPeriod(market, period, pageParam, minute); + }, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) return undefined; + const oldestCandle = lastPage[lastPage.length - 1]; + return oldestCandle?.candle_date_time_utc; + }, + initialPageParam: undefined, + select: (data) => ({ + candles: data.pages.flat(), + hasNextPage: data.pages[data.pages.length - 1]?.length === 200, + }), + }); + + return { + refetch, + data, + fetchNextPage, + hasNextPage, + }; +} diff --git a/packages/client/src/hooks/chart/useRealTimeCandle.ts b/packages/client/src/hooks/chart/useRealTimeCandle.ts new file mode 100644 index 00000000..70808099 --- /dev/null +++ b/packages/client/src/hooks/chart/useRealTimeCandle.ts @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react'; +import { CandleFormat, CandlePeriod } from '@/types/chart'; +import { ISeriesApi } from 'lightweight-charts'; +import { getCurrentCandleStartTime } from '@/utility/chart/chartTimeUtils'; + +type Props = { + seriesRef: React.RefObject>; + currentPrice: number; + activePeriod: CandlePeriod; + refetch: () => Promise; + minute?: number; +}; +export function useRealTimeCandle({ + seriesRef, + currentPrice, + activePeriod, + refetch, + minute, +}: Props) { + const lastCandleRef = useRef(null); + + const updateRealTimeCandle = () => { + if (!seriesRef.current || !currentPrice || !lastCandleRef.current) return; + + const currentCandleStartTime = getCurrentCandleStartTime( + activePeriod, + minute, + ); + if ( + !lastCandleRef.current || + lastCandleRef.current.time !== currentCandleStartTime + ) { + refetch(); + } else { + const updatedCandle = { + ...lastCandleRef.current, + close: currentPrice, + high: Math.max(lastCandleRef.current.high, currentPrice), + low: Math.min(lastCandleRef.current.low, currentPrice), + }; + lastCandleRef.current = updatedCandle; + seriesRef.current.update(updatedCandle); + } + }; + + useEffect(() => { + if (!seriesRef.current || !currentPrice) return; + updateRealTimeCandle(); + }, [currentPrice, minute, activePeriod]); + + return { lastCandleRef }; +} diff --git a/packages/client/src/hooks/market/usePeriodChart.ts b/packages/client/src/hooks/market/usePeriodChart.ts deleted file mode 100644 index 4a149341..00000000 --- a/packages/client/src/hooks/market/usePeriodChart.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getCandleByPeriod } from '@/api/market'; -import { Candle, CandlePeriod, InfiniteCandle } from '@/types/chart'; -import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; - -export function usePeriodChart( - market: string, - period: CandlePeriod, - minute?: number, -) { - const { data, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery< - Candle[], - Error, - InfiniteCandle, - [string, string, CandlePeriod, number?], - string | undefined - >({ - queryKey: ['candles', market, period, minute], - queryFn: ({ pageParam }) => { - return getCandleByPeriod(market, period, pageParam, minute); - }, - getNextPageParam: (lastPage) => { - if (lastPage.length === 0) return undefined; - const oldestCandle = lastPage[lastPage.length - 1]; - return oldestCandle?.candle_date_time_utc; - }, - initialPageParam: undefined, - select: (data) => ({ - candles: data.pages.flat(), - hasNextPage: data.pages[data.pages.length - 1]?.length === 200, - }), - }); - - return { - data, - fetchNextPage, - hasNextPage, - }; -} diff --git a/packages/client/src/hooks/market/useValidCoin.ts b/packages/client/src/hooks/market/useValidCoin.ts new file mode 100644 index 00000000..321117dd --- /dev/null +++ b/packages/client/src/hooks/market/useValidCoin.ts @@ -0,0 +1,13 @@ +import { useMarketAll } from '@/hooks/market/useMarketAll'; +import { useMemo } from 'react'; +import { filterCoin } from '@/utility/validation/filter'; +export function useValidCoin(market: string | undefined) { + const { data } = useMarketAll(); + const KRW_Markets = useMemo(() => filterCoin(data, 'KRW'), [data]); + const isValidCoin = useMemo(() => { + if (!market || !KRW_Markets) return false; + return KRW_Markets.some((item) => item.market === market); + }, [KRW_Markets]); + + return { isValidCoin }; +} diff --git a/packages/client/src/hooks/ui/useSideDraw.ts b/packages/client/src/hooks/ui/useSideDraw.ts index 383e5e47..38cce56a 100644 --- a/packages/client/src/hooks/ui/useSideDraw.ts +++ b/packages/client/src/hooks/ui/useSideDraw.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { SideBarCategory } from '@/types/category'; -import { isSideBarMenu } from '@/utility/typeGuard'; +import { isSideBarMenu } from '@/utility/validation/typeGuard'; function useSideDrawer() { const [activeMenu, setActiveMenu] = useState(null); diff --git a/packages/client/src/hooks/ui/useToast.ts b/packages/client/src/hooks/ui/useToast.ts index f14ca586..40c57541 100644 --- a/packages/client/src/hooks/ui/useToast.ts +++ b/packages/client/src/hooks/ui/useToast.ts @@ -6,6 +6,12 @@ export function useToast() { pauseOnHover: false, hideProgressBar: true, transition: Zoom, + style: { + width: 'fit-content', + whiteSpace: 'nowrap', + display: 'inline-flex', + margin: '0 auto', + }, }; const showToast = { diff --git a/packages/client/src/hooks/useMyHistory.ts b/packages/client/src/hooks/useMyHistory.ts index 515ef262..1718422e 100644 --- a/packages/client/src/hooks/useMyHistory.ts +++ b/packages/client/src/hooks/useMyHistory.ts @@ -1,16 +1,15 @@ import { myHistory } from '@/api/history'; -import { getCookie } from '@/utility/cookies'; +import { getCookie } from '@/utility/storage/cookies'; import { useSuspenseQuery } from '@tanstack/react-query'; export function useMyHistory() { const QUERY_KEY = 'MY_History'; const token = getCookie('access_token'); - const {data} = useSuspenseQuery({ + const { data } = useSuspenseQuery({ queryFn: () => myHistory(token), queryKey: [QUERY_KEY], refetchOnMount: 'always', }); - return data; } diff --git a/packages/client/src/pages/account/balance/BalanceCoin.tsx b/packages/client/src/pages/account/balance/BalanceCoin.tsx index e60c9c24..da8088d4 100644 --- a/packages/client/src/pages/account/balance/BalanceCoin.tsx +++ b/packages/client/src/pages/account/balance/BalanceCoin.tsx @@ -1,7 +1,7 @@ import { Change, CoinTicker } from '@/types/ticker'; import colorClasses from '@/constants/priceColor'; import { AccountCoin } from '@/types/account'; -import PORTFOLIO_EVALUATOR from '@/utility/portfolioEvaluator'; +import PORTFOLIO_EVALUATOR from '@/utility/finance/portfolioEvaluator'; import { Link } from 'react-router-dom'; type BalanceCoinProps = { diff --git a/packages/client/src/pages/account/balance/BalanceTable.tsx b/packages/client/src/pages/account/balance/BalanceTable.tsx index ae27f771..2855cc97 100644 --- a/packages/client/src/pages/account/balance/BalanceTable.tsx +++ b/packages/client/src/pages/account/balance/BalanceTable.tsx @@ -1,7 +1,7 @@ import BalanceInfo from '@/pages/account/balance/BalanceInfo'; import { BalanceMarket } from '@/types/market'; import { SSEDataType } from '@/types/ticker'; -import PORTFOLIO_EVALUATOR from '@/utility/portfolioEvaluator'; +import PORTFOLIO_EVALUATOR from '@/utility/finance/portfolioEvaluator'; type BalanceTableProps = { KRW: number; diff --git a/packages/client/src/pages/auth/Redirect.tsx b/packages/client/src/pages/auth/Redirect.tsx index 83cfdb82..45631956 100644 --- a/packages/client/src/pages/auth/Redirect.tsx +++ b/packages/client/src/pages/auth/Redirect.tsx @@ -1,6 +1,6 @@ import { useSearchParams, useNavigate } from 'react-router-dom'; import { useEffect } from 'react'; -import { setCookie } from '@/utility/cookies'; +import { setCookie } from '@/utility/storage/cookies'; import { useAuthStore } from '@/store/authStore'; function Redirect() { diff --git a/packages/client/src/pages/home/components/Coin.tsx b/packages/client/src/pages/home/components/Coin.tsx index e30b14ba..d3f980b2 100644 --- a/packages/client/src/pages/home/components/Coin.tsx +++ b/packages/client/src/pages/home/components/Coin.tsx @@ -19,7 +19,8 @@ function Coin({ formatters, market, sseData }: CoinProps) { const { addRecentlyViewedMarket } = useRecentlyMarketStore(); const handleClick = () => { addRecentlyViewedMarket(market.market); - navigate(`/trade/${market.market}`); + console.log(market); + navigate(`/trade/KRW-${market.market.split('-')[1]}`); queryClient.invalidateQueries({ queryKey: ['recentlyMarketList'] }); }; const change: Change = sseData[market.market]?.change; diff --git a/packages/client/src/pages/home/components/CoinList.tsx b/packages/client/src/pages/home/components/CoinList.tsx index 5a28d555..b5eea53e 100644 --- a/packages/client/src/pages/home/components/CoinList.tsx +++ b/packages/client/src/pages/home/components/CoinList.tsx @@ -21,7 +21,7 @@ function CoinList({ markets, activeCategory }: CoinListProps) { COINS_PER_PAGE * (currentScrollPage - 1), COINS_PER_PAGE * currentScrollPage, ), - [currentScrollPage], + [currentScrollPage, activeCategory], ); useEffect(() => { diff --git a/packages/client/src/pages/home/components/CoinView.tsx b/packages/client/src/pages/home/components/CoinView.tsx index 9cedb0c8..6a676ca7 100644 --- a/packages/client/src/pages/home/components/CoinView.tsx +++ b/packages/client/src/pages/home/components/CoinView.tsx @@ -3,8 +3,8 @@ import CoinCategories from '@/pages/home/components/CoinCategories'; import CoinList from '@/pages/home/components/CoinList'; import { MarketData } from '@/types/market'; import { MarketCategory } from '@/types/category'; -import { filterCoin } from '@/utility/filter'; -import { isMarket } from '@/utility/typeGuard'; +import { filterCoin } from '@/utility/validation/filter'; +import { isMarket } from '@/utility/validation/typeGuard'; import { useState } from 'react'; function CoinView() { diff --git a/packages/client/src/pages/layout/Layout.tsx b/packages/client/src/pages/layout/Layout.tsx index 57dc8686..dd4c9605 100644 --- a/packages/client/src/pages/layout/Layout.tsx +++ b/packages/client/src/pages/layout/Layout.tsx @@ -4,17 +4,20 @@ import { Outlet } from 'react-router-dom'; function Layout() { return ( - <> -
-
-
+
+
+
+
-
+ +
+ +
- +
); } diff --git a/packages/client/src/pages/trade/Trade.tsx b/packages/client/src/pages/trade/Trade.tsx index 11601378..6e0126f1 100644 --- a/packages/client/src/pages/trade/Trade.tsx +++ b/packages/client/src/pages/trade/Trade.tsx @@ -2,42 +2,59 @@ import Chart from '@/pages/trade/components/chart/Chart'; import OrderBook from '@/pages/trade/components/order_book/OrderBook'; import OrderForm from '@/pages/trade/components/order_form/OrderForm'; import TradeHeader from '@/pages/trade/components/trade_header/TradeHeader'; -import { useParams } from 'react-router-dom'; -import { useSSETicker } from '@/hooks/SSE/useSSETicker'; -import { Suspense, useMemo, useState } from 'react'; import ChartSkeleton from '@/pages/trade/components/chart/ChartSkeleton'; import TradeFooter from '@/pages/trade/components/trade_footer/TradeFooter'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useSSETicker } from '@/hooks/SSE/useSSETicker'; +import { Suspense, useMemo, useState, useCallback } from 'react'; +import { useToast } from '@/hooks/ui/useToast'; +import { useEffect } from 'react'; +import { useValidCoin } from '@/hooks/market/useValidCoin'; function Trade() { const { market } = useParams(); + const toast = useToast(); + const navigate = useNavigate(); + const marketCode = useMemo(() => (market ? [{ market }] : []), [market]); const { sseData: price } = useSSETicker(marketCode); const [selectPrice, setSelectPrice] = useState(null); + const { isValidCoin } = useValidCoin(market); - if (!market || !price) return; + useEffect(() => { + if (!isValidCoin) { + toast.error('원화로 거래 불가능한 코인이에요'); + navigate('/'); + } + }, [isValidCoin]); - const currentPrice = price[market]?.trade_price; - const handleSelectPrice = (price: number) => { + const handleSelectPrice = useCallback((price: number) => { setSelectPrice(price); - }; + }, []); + + if (!market || !price) return null; + const currentPrice = price[market]?.trade_price; + return ( -
- -
- }> - - - - + <> +
+ +
+ }> + + + + +
-
+ ); } diff --git a/packages/client/src/pages/trade/components/chart/CandleChart.tsx b/packages/client/src/pages/trade/components/chart/CandleChart.tsx index 485eff3a..3935ab57 100644 --- a/packages/client/src/pages/trade/components/chart/CandleChart.tsx +++ b/packages/client/src/pages/trade/components/chart/CandleChart.tsx @@ -1,62 +1,41 @@ -import { useRef, useEffect } from 'react'; +import { useEffect } from 'react'; import { Candle, CandlePeriod } from '@/types/chart'; -import { IChartApi, ISeriesApi } from 'lightweight-charts'; -import { - initializeChart, - setupCandlestickSeries, -} from '@/pages/trade/components/chart/chartSetup'; -import { chartConfig } from '@/pages/trade/components/chart/config'; import { formatCandleData } from '@/utility/format/formatCandleData'; -import { - handleResize, - handleScroll, -} from '@/pages/trade/components/chart/chartEvent'; +import { handleScroll } from '@/utility/chart/chartEvent'; +import { useRealTimeCandle } from '@/hooks/chart/useRealTimeCandle'; +import { useChartSetup } from '@/hooks/chart/useChartSetup'; type CandleChartProps = { activePeriod: CandlePeriod; minute: number | undefined; data: Candle[]; + refetch: () => Promise; fetchNextPage: () => Promise; + currentPrice: number; }; function CandleChart({ activePeriod, minute, data, + refetch, fetchNextPage, + currentPrice, }: CandleChartProps) { - const chartRef = useRef(null); - const chartInstanceRef = useRef(null); - const seriesRef = useRef | null>(null); - - useEffect(() => { - if (!chartRef.current) return; - chartInstanceRef.current = initializeChart(chartRef.current, chartConfig); - seriesRef.current = setupCandlestickSeries( - chartInstanceRef.current, - [], - chartConfig, - ); - const resizeObserver = new ResizeObserver(() => { - handleResize(chartRef, chartInstanceRef); - }); - - if (chartRef.current.parentElement) { - resizeObserver.observe(chartRef.current.parentElement); - } - - return () => { - if (chartInstanceRef.current) { - resizeObserver.disconnect(); - chartInstanceRef.current.remove(); - } - }; - }, []); + const { chartRef, chartInstanceRef, seriesRef } = useChartSetup(); + const { lastCandleRef } = useRealTimeCandle({ + seriesRef, + currentPrice, + activePeriod, + refetch, + minute, + }); useEffect(() => { if (!seriesRef.current || !chartInstanceRef.current) return; const formattedData = formatCandleData(data); seriesRef.current.setData(formattedData); + lastCandleRef.current = formattedData[formattedData.length - 1]; }, [data]); useEffect(() => { diff --git a/packages/client/src/pages/trade/components/chart/Chart.tsx b/packages/client/src/pages/trade/components/chart/Chart.tsx index eba3bac2..d1120313 100644 --- a/packages/client/src/pages/trade/components/chart/Chart.tsx +++ b/packages/client/src/pages/trade/components/chart/Chart.tsx @@ -1,13 +1,22 @@ -import { usePeriodChart } from '@/hooks/market/usePeriodChart'; +import { usePeriodChart } from '@/hooks/chart/usePeriodChart'; import { useState } from 'react'; import ChartSelector from '@/pages/trade/components/chart/ChartSelector'; import { CandlePeriod } from '@/types/chart'; import CandleChart from '@/pages/trade/components/chart/CandleChart'; -function Chart({ market }: { market: string }) { +type ChartProps = { + market: string; + currentPrice: number; +}; + +function Chart({ market, currentPrice }: ChartProps) { const [activePeriod, setActivePeriod] = useState('days'); const [minute, setMinute] = useState(); - const { data, fetchNextPage } = usePeriodChart(market, activePeriod, minute); + const { data, refetch, fetchNextPage } = usePeriodChart( + market, + activePeriod, + minute, + ); const handleActivePeriod = (period: CandlePeriod, minute?: number) => { setActivePeriod(period); @@ -15,7 +24,7 @@ function Chart({ market }: { market: string }) { }; return ( -
+
); diff --git a/packages/client/src/pages/trade/components/order_book/OrderBook.tsx b/packages/client/src/pages/trade/components/order_book/OrderBook.tsx index 8bc893f1..aea7f44b 100644 --- a/packages/client/src/pages/trade/components/order_book/OrderBook.tsx +++ b/packages/client/src/pages/trade/components/order_book/OrderBook.tsx @@ -29,7 +29,7 @@ function OrderBook({ const bids = formatBids(orderBook[market]); return ( -
+
호가
diff --git a/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx index 841f3487..01854de6 100644 --- a/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx +++ b/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx @@ -36,7 +36,7 @@ function OrderBuyForm({ currentPrice, selectPrice }: OrderBuyFormProsp) {
+
주문하기
{TABS.map((tab) => ( diff --git a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx index b1a857ae..724ee85b 100644 --- a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx +++ b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx @@ -7,7 +7,7 @@ import { useCheckCoin } from '@/hooks/trade/useCheckCoin'; import { useMarketParams } from '@/hooks/market/useMarketParams'; import { useOrderForm } from '@/hooks/trade/useOrderForm'; import { useMyAccount } from '@/hooks/auth/useMyAccount'; -import { calculateProfitInfo } from '@/utility/calculateProfit'; +import { calculateProfitInfo } from '@/utility/finance/calculateProfit'; type OrderSellFormProps = { currentPrice: number; @@ -56,14 +56,14 @@ function OrderSellForm({ currentPrice, selectPrice }: OrderSellFormProps) {
+
-
+
{duplicatedCoins?.map((coin, index) => ( +
{ if (chartRef.current && chartInstanceRef.current) { const { width } = - chartRef.current.parentElement?.getBoundingClientRect() || { width: 0 }; + chartRef.current.parentElement?.getBoundingClientRect() || { + width: 0, + }; chartInstanceRef.current.applyOptions({ width: width, }); diff --git a/packages/client/src/pages/trade/components/chart/chartSetup.ts b/packages/client/src/utility/chart/chartSetup.ts similarity index 92% rename from packages/client/src/pages/trade/components/chart/chartSetup.ts rename to packages/client/src/utility/chart/chartSetup.ts index 3b997be6..05f10784 100644 --- a/packages/client/src/pages/trade/components/chart/chartSetup.ts +++ b/packages/client/src/utility/chart/chartSetup.ts @@ -1,7 +1,7 @@ // src/components/Chart/utils/chartSetup.ts import { IChartApi, createChart, ISeriesApi } from 'lightweight-charts'; import { CandleFormat } from '@/types/chart'; -import { ChartConfig } from '@/pages/trade/components/chart/config'; +import { ChartConfig } from '@/utility/chart/config'; export const initializeChart = ( container: HTMLElement, diff --git a/packages/client/src/utility/chart/chartTimeUtils.ts b/packages/client/src/utility/chart/chartTimeUtils.ts new file mode 100644 index 00000000..c070ddc3 --- /dev/null +++ b/packages/client/src/utility/chart/chartTimeUtils.ts @@ -0,0 +1,55 @@ +import { CandlePeriod } from '@/types/chart'; +import { Time } from 'lightweight-charts'; + +export const getPeriodMs = (activePeriod: CandlePeriod, minute?: number) => { + switch (activePeriod) { + case 'minutes': + return (minute || 1) * 60 * 1000; + case 'days': + return 24 * 60 * 60 * 1000; + case 'weeks': + return 7 * 24 * 60 * 60 * 1000; + case 'months': + return 30 * 24 * 60 * 60 * 1000; + default: + return 60 * 1000; + } +}; + +export const getCurrentCandleStartTime = ( + activePeriod: CandlePeriod, + minute?: number, +) => { + const now = new Date(); + const periodMs = getPeriodMs(activePeriod, minute); + + switch (activePeriod) { + case 'minutes': + return ((Math.floor(now.getTime() / periodMs) * periodMs) / 1000) as Time; + + case 'days': { + const startOfDay = new Date(now); + startOfDay.setUTCHours(0, 0, 0, 0); + return (startOfDay.getTime() / 1000) as Time; + } + + case 'weeks': { + const startOfWeek = new Date(now); + startOfWeek.setUTCHours(0, 0, 0, 0); + const day = startOfWeek.getUTCDay(); + const diff = startOfWeek.getUTCDate() - day + (day === 0 ? -6 : 1); + startOfWeek.setUTCDate(diff); + return (startOfWeek.getTime() / 1000) as Time; + } + + case 'months': { + const startOfMonth = new Date(now); + startOfMonth.setUTCHours(0, 0, 0, 0); + startOfMonth.setUTCDate(1); + return (startOfMonth.getTime() / 1000) as Time; + } + + default: + return ((Math.floor(now.getTime() / periodMs) * periodMs) / 1000) as Time; + } +}; diff --git a/packages/client/src/pages/trade/components/chart/config.ts b/packages/client/src/utility/chart/config.ts similarity index 92% rename from packages/client/src/pages/trade/components/chart/config.ts rename to packages/client/src/utility/chart/config.ts index 68817a1c..754e3132 100644 --- a/packages/client/src/pages/trade/components/chart/config.ts +++ b/packages/client/src/utility/chart/config.ts @@ -20,6 +20,10 @@ export const chartConfig = { vertLines: { color: '#1111' }, horzLines: { color: '#1111' }, }, + timeScale: { + rightOffset: 5, + barSpacing: 10, + }, }, candleStickOptions: { wickUpColor: 'rgb(225, 50, 85)', diff --git a/packages/client/src/utility/calculateProfit.ts b/packages/client/src/utility/finance/calculateProfit.ts similarity index 100% rename from packages/client/src/utility/calculateProfit.ts rename to packages/client/src/utility/finance/calculateProfit.ts diff --git a/packages/client/src/utility/order.ts b/packages/client/src/utility/finance/calculateTotalPrice.ts similarity index 100% rename from packages/client/src/utility/order.ts rename to packages/client/src/utility/finance/calculateTotalPrice.ts diff --git a/packages/client/src/utility/portfolioEvaluator.ts b/packages/client/src/utility/finance/portfolioEvaluator.ts similarity index 100% rename from packages/client/src/utility/portfolioEvaluator.ts rename to packages/client/src/utility/finance/portfolioEvaluator.ts diff --git a/packages/client/src/utility/format/formatCandleData.ts b/packages/client/src/utility/format/formatCandleData.ts index 403c9c1b..cda0aeb5 100644 --- a/packages/client/src/utility/format/formatCandleData.ts +++ b/packages/client/src/utility/format/formatCandleData.ts @@ -1,10 +1,10 @@ -import { Candle, CandleFormat } from '@/types/chart'; import { Time } from 'lightweight-charts'; - +import { Candle, CandleFormat } from '@/types/chart'; export function formatCandleData(data: Candle[]): CandleFormat[] { const uniqueData = data.reduce( (acc, current) => { - const timeKey = new Date(current.candle_date_time_kst).getTime(); + const date = new Date(current.candle_date_time_kst); + const timeKey = date.getTime() + 9 * 60 * 60 * 1000; acc[timeKey] = current; return acc; }, @@ -12,13 +12,17 @@ export function formatCandleData(data: Candle[]): CandleFormat[] { ); const sortedData = Object.values(uniqueData).sort((a, b) => { - const dateA = new Date(a.candle_date_time_kst).getTime(); - const dateB = new Date(b.candle_date_time_kst).getTime(); + const dateA = + new Date(a.candle_date_time_kst).getTime() + 9 * 60 * 60 * 1000; + const dateB = + new Date(b.candle_date_time_kst).getTime() + 9 * 60 * 60 * 1000; return dateA - dateB; }); const formattedData = sortedData.map((candle) => ({ - time: (new Date(candle.candle_date_time_kst).getTime() / 1000) as Time, + time: ((new Date(candle.candle_date_time_kst).getTime() + + 9 * 60 * 60 * 1000) / + 1000) as Time, open: candle.opening_price, high: candle.high_price, low: candle.low_price, diff --git a/packages/client/src/utility/cookies.ts b/packages/client/src/utility/storage/cookies.ts similarity index 100% rename from packages/client/src/utility/cookies.ts rename to packages/client/src/utility/storage/cookies.ts diff --git a/packages/client/src/utility/recentlyMarket.ts b/packages/client/src/utility/storage/recentlyMarket.ts similarity index 100% rename from packages/client/src/utility/recentlyMarket.ts rename to packages/client/src/utility/storage/recentlyMarket.ts diff --git a/packages/client/src/utility/filter.ts b/packages/client/src/utility/validation/filter.ts similarity index 100% rename from packages/client/src/utility/filter.ts rename to packages/client/src/utility/validation/filter.ts diff --git a/packages/client/src/utility/typeGuard.ts b/packages/client/src/utility/validation/typeGuard.ts similarity index 100% rename from packages/client/src/utility/typeGuard.ts rename to packages/client/src/utility/validation/typeGuard.ts diff --git a/packages/server/package.json b/packages/server/package.json index 6a9d36f6..cecfd906 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -25,6 +25,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.7", "@nestjs/schedule": "^4.1.1", @@ -41,6 +42,9 @@ "ioredis": "^5.4.1", "js-yaml": "^4.1.0", "mysql2": "^3.11.3", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-kakao": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "tunnel-ssh": "^5.1.2", @@ -58,6 +62,9 @@ "@types/jest": "^29.5.2", "@types/js-yaml": "^4", "@types/node": "^20.3.1", + "@types/passport": "^0", + "@types/passport-google-oauth20": "^2", + "@types/passport-kakao": "^1", "@types/socket.io": "^3.0.2", "@types/supertest": "^6.0.0", "@types/ws": "^8.5.13", diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index b2214dcd..f9a61ced 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -1,69 +1,151 @@ import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpStatus, - Post, - Request, - UseGuards, + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Post, + Request, + Res, + UseGuards, } from '@nestjs/common'; import { AuthGuard } from './auth.guard'; +import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; import { - ApiBody, - ApiBearerAuth, - ApiSecurity, - ApiResponse, + ApiBody, + ApiBearerAuth, + ApiSecurity, + ApiResponse, } from '@nestjs/swagger'; import { SignInDto } from './dtos/sign-in.dto'; import { SignUpDto } from './dtos/sign-up.dto'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} - - @ApiBody({ type: SignInDto }) - @HttpCode(HttpStatus.OK) - @Post('login') - signIn(@Body() signInDto: Record) { - return this.authService.signIn(signInDto.username); - } - - @HttpCode(HttpStatus.OK) - @Post('guest-login') - guestSignIn() { - return this.authService.guestSignIn(); - } - - @ApiResponse({ - status: HttpStatus.OK, - description: 'New user successfully registered', - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Invalid input or user already exists', - }) - @HttpCode(HttpStatus.CREATED) - @Post('signup') - async signUp(@Body() signUpDto: SignUpDto) { - return this.authService.signUp(signUpDto.username); - } - - @ApiBearerAuth('access-token') - @ApiSecurity('access-token') - @UseGuards(AuthGuard) - @Delete('logout') - logout(@Request() req) { - return this.authService.logout(req.user.userId); - } - - @UseGuards(AuthGuard) - @ApiBearerAuth('access-token') - @ApiSecurity('access-token') - @Get('profile') - getProfile(@Request() req) { - return req.user; - } + constructor(private authService: AuthService) {} + + @ApiBody({ type: SignInDto }) + @HttpCode(HttpStatus.OK) + @Post('login') + signIn(@Body() signInDto: Record) { + return this.authService.signIn(signInDto.username); + } + + @HttpCode(HttpStatus.OK) + @Post('guest-login') + guestSignIn() { + return this.authService.guestSignIn(); + } + + @Get('google') + @UseGuards(PassportAuthGuard('google')) + async googleLogin() {} + + @Get('google/callback') + @UseGuards(PassportAuthGuard('google')) + async googleLoginCallback(@Request() req, @Res() res): Promise { + const googleUser = req.user; + + const signUpDto: SignUpDto = { + name: googleUser.name, + email: googleUser.email, + provider: googleUser.provider, + providerId: googleUser.id, + isGuest: false, + }; + + const tokens = await this.authService.validateOAuthLogin(signUpDto); + // 요청 Origin 기반으로 리다이렉트 URL 결정 + const origin = req.headers['origin']; + const frontendURL = + origin && origin.includes('localhost') + ? 'http://localhost:5173' + : 'https://www.corinee.site'; + const redirectURL = new URL('/auth/callback', frontendURL); + + redirectURL.searchParams.append('access_token', tokens.access_token); + redirectURL.searchParams.append('refresh_token', tokens.refresh_token); + console.log(redirectURL); + return res.redirect(redirectURL.toString()); + } + + @Get('kakao') + @UseGuards(PassportAuthGuard('kakao')) + async kakaoLogin() {} + + @Get('kakao/callback') + @UseGuards(PassportAuthGuard('kakao')) + async kakaoLoginCallback(@Request() req, @Res() res) { + const kakaoUser = req.user; + + const signUpDto: SignUpDto = { + name: kakaoUser.name, + email: kakaoUser.email, + provider: kakaoUser.provider, + providerId: kakaoUser.id, + isGuest: false, + }; + + const tokens = await this.authService.validateOAuthLogin(signUpDto); + + const origin = req.headers['origin']; + const frontendURL = + origin && origin.includes('localhost') + ? 'http://localhost:5173' + : 'https://www.corinee.site'; + const redirectURL = new URL('/auth/callback', frontendURL); + redirectURL.searchParams.append('access_token', tokens.access_token); + redirectURL.searchParams.append('refresh_token', tokens.refresh_token); + return res.redirect(redirectURL.toString()); + } + + @ApiResponse({ + status: HttpStatus.OK, + description: 'New user successfully registered', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input or user already exists', + }) + @HttpCode(HttpStatus.CREATED) + @Post('signup') + async signUp(@Body() signUpDto: SignUpDto) { + return this.authService.signUp(signUpDto); + } + + @ApiBearerAuth('access-token') + @ApiSecurity('access-token') + @UseGuards(AuthGuard) + @Delete('logout') + logout(@Request() req) { + return this.authService.logout(req.user.userId); + } + + @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') + @ApiSecurity('access-token') + @Get('profile') + getProfile(@Request() req) { + return req.user; + } + + @ApiBody({ + schema: { + type: 'object', + properties: { + refreshToken: { + type: 'string', + description: 'Refresh token used for renewing access token', + example: 'your-refresh-token', + }, + }, + }, + }) + @HttpCode(HttpStatus.OK) + @Post('refresh') + refreshTokens(@Body() body: { refreshToken: string }) { + return this.authService.refreshTokens(body.refreshToken); + } } diff --git a/packages/server/src/auth/auth.guard.ts b/packages/server/src/auth/auth.guard.ts index 025a47e0..705989d6 100644 --- a/packages/server/src/auth/auth.guard.ts +++ b/packages/server/src/auth/auth.guard.ts @@ -22,8 +22,6 @@ export class AuthGuard implements CanActivate { const payload = await this.jwtService.verifyAsync(token, { secret: jwtConstants.secret, }); - // 💡 We're assigning the payload to the request object here - // so that we can access it in our route handlers request['user'] = payload; } catch { throw new UnauthorizedException(); diff --git a/packages/server/src/auth/auth.module.ts b/packages/server/src/auth/auth.module.ts index 3dafe996..dbce4048 100644 --- a/packages/server/src/auth/auth.module.ts +++ b/packages/server/src/auth/auth.module.ts @@ -8,18 +8,29 @@ import { AuthService } from './auth.service'; import { AccountRepository } from 'src/account/account.repository'; import { AuthController } from './auth.controller'; import { AccountModule } from 'src/account/account.module'; +import { KakaoStrategy } from './strategies/kakao.strategy'; +import { GoogleStrategy } from './strategies/google.strategy'; +import { PassportModule } from '@nestjs/passport'; @Module({ - imports: [ - TypeOrmModule.forFeature([User]), - JwtModule.register({ - global: true, - secret: jwtConstants.secret, - signOptions: { expiresIn: '6000s' }, - }), - AccountModule, - ], - providers: [UserRepository, AccountRepository, AuthService, JwtService], - controllers: [AuthController], - exports: [UserRepository], + imports: [ + TypeOrmModule.forFeature([User]), + JwtModule.register({ + global: true, + secret: jwtConstants.secret, + signOptions: { expiresIn: '6000s' }, + }), + AccountModule, + PassportModule + ], + providers: [ + UserRepository, + AccountRepository, + AuthService, + JwtService, + GoogleStrategy, + KakaoStrategy, + ], + controllers: [AuthController], + exports: [UserRepository], }) export class AuthModule {} diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index d3375bc9..11774387 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -1,16 +1,25 @@ -import { ConflictException, Injectable } from '@nestjs/common'; +import { + ConflictException, + ForbiddenException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { UserRepository } from './user.repository'; import { JwtService } from '@nestjs/jwt'; import { + ACCESS_TOKEN_TTL, DEFAULT_BTC, DEFAULT_KRW, DEFAULT_USDT, + GUEST_ID_TTL, + REFRESH_TOKEN_TTL, jwtConstants, } from './constants'; import { v4 as uuidv4 } from 'uuid'; import { AccountRepository } from 'src/account/account.repository'; import { RedisRepository } from 'src/redis/redis.repository'; import { User } from './user.entity'; +import { SignUpDto } from './dtos/sign-up.dto'; @Injectable() export class AuthService { constructor( @@ -22,54 +31,62 @@ export class AuthService { this.createAdminUser(); } - async signIn(username: string): Promise<{ access_token: string }> { + async signIn( + username: string, + ): Promise<{ access_token: string; refresh_token: string }> { const user = await this.userRepository.findOneBy({ username }); - const payload = { userId: user.id, userName: user.username }; - return { - access_token: await this.jwtService.signAsync(payload, { - secret: jwtConstants.secret, - expiresIn: '1d', - }), - }; + if (!user) { + throw new UnauthorizedException('Invalid credentials'); + } + return this.generateTokens(user.id, user.username); } - async guestSignIn(): Promise<{ access_token: string }> { - try{ - const username = `guest_${uuidv4()}`; - - await this.signUp(username, true); + async guestSignIn(): Promise<{ + access_token: string; + refresh_token: string; + }> { + const guestName = `guest_${uuidv4()}`; + const user = { name: guestName, isGuest: true }; - const guestUser = await this.userRepository.findOneBy({ username }); + await this.signUp(user); - await this.redisRepository.setAuthData( - `guest:${guestUser.id}`, - JSON.stringify({ userId: guestUser.id }), - 6000, - ); + const guestUser = await this.userRepository.findOneBy({ + username: guestName, + }); - const payload = { userId: guestUser.id, userName: guestUser.username }; - return { - access_token: await this.jwtService.signAsync(payload, { - secret: jwtConstants.secret, - expiresIn: '1d', - }), - }; - }catch(error){ - console.error(error) - } + await this.redisRepository.setAuthData( + `guest:${guestUser.id}`, + JSON.stringify({ userId: guestUser.id }), + GUEST_ID_TTL, + ); + + return this.generateTokens(guestUser.id, guestUser.username); } - async signUp( - username: string, - isGuest = false, - ): Promise<{ message: string }> { - const existingUser = await this.userRepository.findOneBy({ username }); + async signUp(user: { + name: string; + email?: string; + provider?: string; + providerId?: string; + isGuest?: boolean; + }): Promise<{ message: string }> { + const { name, email, provider, providerId, isGuest } = user; + + const existingUser = isGuest + ? await this.userRepository.findOneBy({ username: name }) + : await this.userRepository.findOne({ + where: { provider, providerId }, + }); + if (existingUser) { - throw new ConflictException('Username already exists'); + throw new ConflictException('User already exists'); } const newUser = await this.userRepository.save({ - username, + username: name, + email, + provider, + providerId, isGuest, }); @@ -87,20 +104,118 @@ export class AuthService { }; } + async validateOAuthLogin( + signUpDto: SignUpDto, + ): Promise<{ access_token: string; refresh_token: string }> { + const { name, email, provider, providerId, isGuest } = signUpDto; + + let user = await this.userRepository.findOne({ + where: { provider, providerId }, + }); + + if (!user) { + await this.signUp( + { name, email, provider, providerId, isGuest: false }, + ); + user = await this.userRepository.findOne({ + where: { provider, providerId }, + }); + } + + if (!user) { + throw new UnauthorizedException('OAuth user creation failed'); + } + + return this.generateTokens(user.id, user.username); + } + + private async generateTokens( + userId: number, + username: string, + ): Promise<{ access_token: string; refresh_token: string }> { + const payload = { userId, userName: username }; + + const accessToken = await this.jwtService.signAsync(payload, { + secret: jwtConstants.secret, + expiresIn: ACCESS_TOKEN_TTL, + }); + + const refreshToken = await this.jwtService.signAsync( + { userId }, + { + secret: jwtConstants.refreshSecret, + expiresIn: REFRESH_TOKEN_TTL, + }, + ); + + await this.redisRepository.setAuthData( + `refresh:${userId}`, + refreshToken, + REFRESH_TOKEN_TTL, + ); + + return { + access_token: accessToken, + refresh_token: refreshToken, + }; + } + + async refreshTokens( + refreshToken: string, + ): Promise<{ access_token: string; refresh_token: string }> { + try { + const payload = await this.jwtService.verifyAsync(refreshToken, { + secret: jwtConstants.refreshSecret, + }); + const userId = payload.userId; + + const storedToken = await this.redisRepository.getAuthData( + `refresh:${userId}`, + ); + + if (!storedToken) { + throw new ForbiddenException({ + message: 'Refresh token has expired', + errorCode: 'REFRESH_TOKEN_EXPIRED', + }); + } + + if (storedToken !== refreshToken) { + throw new UnauthorizedException({ + message: 'Invalid refresh token', + errorCode: 'INVALID_REFRESH_TOKEN', + }); + } + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + throw new UnauthorizedException('User not found'); + } + return this.generateTokens(user.id, user.username); + } catch (error) { + throw new UnauthorizedException({ + message: 'Failed to refresh tokens', + errorCode: 'TOKEN_REFRESH_FAILED', + }); + } + } + async logout(userId: number): Promise<{ message: string }> { - try{ + try { const user = await this.userRepository.findOneBy({ id: userId }); if (!user) { throw new Error('User not found'); } + await this.redisRepository.deleteAuthData(`refresh:${userId}`); + if (user.isGuest) { await this.userRepository.delete({ id: userId }); return { message: 'Guest user data successfully deleted' }; } - }catch(error){ - console.error(error) + } catch (error) { + console.error(error); } } diff --git a/packages/server/src/auth/constants.ts b/packages/server/src/auth/constants.ts index 06bd4fce..4d8f03ae 100644 --- a/packages/server/src/auth/constants.ts +++ b/packages/server/src/auth/constants.ts @@ -1,8 +1,15 @@ export const jwtConstants = { secret: - 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.', + 'superSecureAccessTokenSecret', + refreshSecret: + 'superSecureAccessTokenSecret_superSecureAccessTokenSecret', }; export const DEFAULT_KRW = 30000000; export const DEFAULT_USDT = 300000; export const DEFAULT_BTC = 0; + +export const GUEST_ID_TTL = 24 * 3600; + +export const REFRESH_TOKEN_TTL = 7 * 24 * 3600; +export const ACCESS_TOKEN_TTL = '1d'; \ No newline at end of file diff --git a/packages/server/src/auth/dtos/sign-up.dto.ts b/packages/server/src/auth/dtos/sign-up.dto.ts index b3d9b70a..b64fd51b 100644 --- a/packages/server/src/auth/dtos/sign-up.dto.ts +++ b/packages/server/src/auth/dtos/sign-up.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { IsBoolean, IsString } from 'class-validator'; export class SignUpDto { @ApiProperty({ @@ -8,5 +8,17 @@ export class SignUpDto { required: true, }) @IsString() - username: string; + name: string; + + @IsString() + email: string; + + @IsBoolean() + isGuest: boolean; + + @IsString() + provider: string; + + @IsString() + providerId: string; } diff --git a/packages/server/src/auth/strategies/google.strategy.ts b/packages/server/src/auth/strategies/google.strategy.ts new file mode 100644 index 00000000..e2035579 --- /dev/null +++ b/packages/server/src/auth/strategies/google.strategy.ts @@ -0,0 +1,33 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { Strategy, VerifyCallback } from 'passport-google-oauth20'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { + constructor() { + super({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: `${process.env.CALLBACK_URL}/api/auth/google/callback`, + scope: ['email', 'profile'], + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback, + ): Promise { + const { id, displayName, emails } = profile; + const user = { + provider: 'google', + id, + name: displayName, + email: emails?.[0]?.value, + accessToken, + refreshToken, + }; + done(null, user); + } +} diff --git a/packages/server/src/auth/strategies/kakao.strategy.ts b/packages/server/src/auth/strategies/kakao.strategy.ts new file mode 100644 index 00000000..c8010fae --- /dev/null +++ b/packages/server/src/auth/strategies/kakao.strategy.ts @@ -0,0 +1,32 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { Strategy, Profile } from 'passport-kakao'; + +@Injectable() +export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') { + constructor() { + super({ + clientID: process.env.KAKAO_CLIENT_ID, + clientSecret: '', + callbackURL: `${process.env.CALLBACK_URL}/api/auth/kakao/callback` + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: Profile, + done: Function, + ): Promise { + const { id, username, _json } = profile; + const user = { + provider: 'kakao', + id, + name: username, + email: _json.kakao_account?.email, + accessToken, + refreshToken, + }; + done(null, user); + } +} diff --git a/packages/server/src/auth/user.entity.ts b/packages/server/src/auth/user.entity.ts index 4a8b6968..c9f56bad 100644 --- a/packages/server/src/auth/user.entity.ts +++ b/packages/server/src/auth/user.entity.ts @@ -12,7 +12,6 @@ import { Trade } from 'src/trade/trade.entity'; import { TradeHistory } from 'src/trade-history/trade-history.entity'; @Entity() -@Unique(['username']) export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @@ -22,7 +21,16 @@ export class User extends BaseEntity { @Column() username: string; + + @Column({ nullable: true }) + email: string; + + @Column({ nullable: true }) + provider: string; + @Column({ nullable: true }) + providerId: string; + @OneToOne(() => Account, (account) => account.user, { cascade: true, onDelete: 'CASCADE', diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index 2fb2fa8d..828a6213 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -1,9 +1,9 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { - SwaggerModule, - DocumentBuilder, - SwaggerCustomOptions, + SwaggerModule, + DocumentBuilder, + SwaggerCustomOptions, } from '@nestjs/swagger'; import { config } from 'dotenv'; import { setupSshTunnel } from './configs/ssh-tunnel'; @@ -12,39 +12,38 @@ import { AllExceptionsFilter } from 'common/all-exceptions.filter'; config(); async function bootstrap() { - await setupSshTunnel(); - const app = await NestFactory.create(AppModule); - app.enableCors({ - origin: true, - methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', - credentials: true, - }); + await setupSshTunnel(); + const app = await NestFactory.create(AppModule); + app.enableCors({ + origin: true, + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + credentials: true, + }); - const config = new DocumentBuilder() - .setTitle('CorinEE API example') - .setDescription('CorinEE API description') - .setVersion('1.0') - .addTag('corinee') - .addBearerAuth( - { - type: 'http', - scheme: 'bearer', - name: 'Authorization', - in: 'header', - }, - 'access-token', - ) - .build(); - const customOptions: SwaggerCustomOptions = { - swaggerOptions: { - persistAuthorization: true, - }, - }; - const documentFactory = () => SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, documentFactory); + const config = new DocumentBuilder() + .setTitle('CorinEE API example') + .setDescription('CorinEE API description') + .setVersion('1.0') + .addTag('corinee') + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Access token used for authentication', + }, + 'access-token', + ).build(); + const customOptions: SwaggerCustomOptions = { + swaggerOptions: { + persistAuthorization: true, + }, + }; + const documentFactory = () => SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, documentFactory); - app.setGlobalPrefix('api'); - app.useGlobalFilters(new AllExceptionsFilter()); + app.setGlobalPrefix('api'); + app.useGlobalFilters(new AllExceptionsFilter()); await app.listen(process.env.PORT ?? 3000); } diff --git a/packages/server/src/trade-history/trade-history.entity.ts b/packages/server/src/trade-history/trade-history.entity.ts index 2b1f0dbe..de11a547 100644 --- a/packages/server/src/trade-history/trade-history.entity.ts +++ b/packages/server/src/trade-history/trade-history.entity.ts @@ -1,39 +1,41 @@ import { User } from '@src/auth/user.entity'; import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - CreateDateColumn, - UpdateDateColumn, + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, } from 'typeorm'; @Entity() export class TradeHistory { - @PrimaryGeneratedColumn() - tradeHistoryId: number; + @PrimaryGeneratedColumn() + tradeHistoryId: number; - @Column() - assetName: string; + @Column() + assetName: string; - @Column() - tradeType: string; + @Column() + tradeType: string; - @Column() - tradeCurrency: string; + @Column() + tradeCurrency: string; - @Column('double') - price: number; + @Column('double') + price: number; - @Column('double') - quantity: number; + @Column('double') + quantity: number; - @Column({ type: 'timestamp' }) - createdAt: Date; + @Column({ type: 'timestamp' }) + createdAt: Date; - @CreateDateColumn({ type: 'timestamp' }) - tradeDate: Date; + @CreateDateColumn({ type: 'timestamp' }) + tradeDate: Date; - @ManyToOne(() => User, (user) => user.tradeHistories) - user: User; + @ManyToOne(() => User, (user) => user.tradeHistories, { + onDelete: 'CASCADE', + }) + user: User; } diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts index eaa420a2..a1513611 100644 --- a/packages/server/src/trade/trade-ask.service.ts +++ b/packages/server/src/trade/trade-ask.service.ts @@ -4,7 +4,7 @@ import { OnModuleInit, UnprocessableEntityException, } from '@nestjs/common'; -import { DataSource, QueryRunner } from 'typeorm'; +import { DataSource } from 'typeorm'; import { AccountRepository } from 'src/account/account.repository'; import { AssetRepository } from 'src/asset/asset.repository'; import { TradeRepository } from './trade.repository'; @@ -44,7 +44,7 @@ export class AskService implements OnModuleInit { } }) if(!asset) return 0; - return asset.quantity * (percent / 100); + return parseFloat((asset.quantity * (percent / 100)).toFixed(8)); } async createAskTrade(user, askDto) { if(askDto.receivedAmount * askDto.receivedPrice < 5000) throw new BadRequestException(); @@ -67,14 +67,14 @@ export class AskService implements OnModuleInit { }); } const userAsset = await this.checkCurrency(askDto, userAccount, queryRunner) - const assetBalance = userAsset.quantity - askDto.receivedAmount; + const assetBalance = parseFloat((userAsset.quantity - askDto.receivedAmount).toFixed(8)); if(assetBalance <= 0){ await this.assetRepository.delete({ assetId: userAsset.assetId }) }else{ userAsset.quantity = assetBalance - userAsset.price -= Math.floor(askDto.receivedPrice + askDto.receivedAmount) + userAsset.price -= parseFloat(askDto.receivedPrice.toFixed(8)) * parseFloat(askDto.receivedAmount.toFixed(8)) this.assetRepository.updateAssetPrice(userAsset, queryRunner); } await this.tradeRepository.createTrade(askDto, user.userId,'sell', queryRunner); @@ -177,8 +177,8 @@ export class AskService implements OnModuleInit { try { const buyData = { ...tradeData }; buyData.quantity = - tradeData.quantity >= bid_size ? bid_size.toFixed(8) : tradeData.quantity.toFixed(8) - buyData.price = (bid_price * krw).toFixed(8); + tradeData.quantity >= bid_size ? parseFloat(bid_size.toFixed(8)) : parseFloat(tradeData.quantity.toFixed(8)) + buyData.price = parseFloat((bid_price * krw).toFixed(8)); if(buyData.quantity<0.00000001){ await queryRunner.commitTransaction(); return true; @@ -192,7 +192,7 @@ export class AskService implements OnModuleInit { ); if (!asset && tradeData.price > buyData.price) { - asset.price = Math.floor(asset.price + (tradeData.price - buyData.price) * buyData.quantity); + asset.price = parseFloat((asset.price + (tradeData.price - buyData.price) * buyData.quantity).toFixed(8)); await this.assetRepository.updateAssetPrice(asset, queryRunner); } @@ -205,13 +205,13 @@ export class AskService implements OnModuleInit { const BTC_QUANTITY = account.BTC - buyData.quantity await this.accountRepository.updateAccountBTC(account.id, BTC_QUANTITY, queryRunner) } - const change = Math.floor(account[typeReceived] + buyData.price * buyData.quantity) + const change = parseFloat((account[typeReceived] + buyData.price * buyData.quantity).toFixed(8)) await this.accountRepository.updateAccountCurrency(typeReceived, change, account.id, queryRunner) tradeData.quantity -= buyData.quantity; - if (tradeData.quantity === 0) { + if (tradeData.quantity <= 0.00000001) { await this.tradeRepository.deleteTrade(tradeId, queryRunner); } else{ await this.tradeRepository.updateTradeTransaction( diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts index f1708968..e4024385 100644 --- a/packages/server/src/trade/trade-bid.service.ts +++ b/packages/server/src/trade/trade-bid.service.ts @@ -37,7 +37,7 @@ export class BidService implements OnModuleInit { async calculatePercentBuy(user, moneyType: string, percent: number) { const money = await this.accountRepository.getMyMoney(user, moneyType); - return Number(money) * (percent / 100); + return parseFloat((money * (percent / 100)).toFixed(8)); } async createBidTrade(user, bidDto) { if(bidDto.receivedAmount * bidDto.receivedPrice < 5000) throw new BadRequestException(); @@ -62,7 +62,7 @@ export class BidService implements OnModuleInit { const accountBalance = await this.checkCurrency(user, bidDto); await this.accountRepository.updateAccountCurrency( bidDto.typeGiven, - Math.floor(accountBalance), + parseFloat(accountBalance.toFixed(8)), userAccount.id, queryRunner, ); @@ -87,7 +87,7 @@ export class BidService implements OnModuleInit { } async checkCurrency(user, bidDto) { const { typeGiven, receivedPrice, receivedAmount } = bidDto; - const givenAmount = Math.floor(receivedPrice * receivedAmount); + const givenAmount = parseFloat((receivedPrice * receivedAmount).toFixed(8)); const userAccount = await this.accountRepository.findOne({ where: { user: { id: user.userId }, @@ -159,13 +159,12 @@ export class BidService implements OnModuleInit { let result = false; try { const buyData = {...tradeData}; - buyData.quantity = buyData.quantity >= ask_size ? ask_size.toFixed(8) : buyData.quantity.toFixed(8) + buyData.quantity = buyData.quantity >= ask_size ? parseFloat(ask_size.toFixed(8)) : parseFloat(buyData.quantity.toFixed(8)) if(buyData.quantity<0.00000001){ await queryRunner.commitTransaction(); return true; } - buyData.price = (ask_price * krw).toFixed(8); - + buyData.price = parseFloat((ask_price * krw).toFixed(8)); const user = await this.userRepository.getUser(userId); await this.tradeHistoryRepository.createTradeHistory( @@ -179,27 +178,26 @@ export class BidService implements OnModuleInit { }); if (asset) { - asset.price = Math.floor(asset.price + buyData.price * buyData.quantity); - asset.quantity += buyData.quantity; - + asset.price = parseFloat((asset.price + buyData.price * buyData.quantity).toFixed(8)); + asset.quantity += parseFloat(buyData.quantity.toFixed(8)); await this.assetRepository.updateAssetQuantityPrice(asset, queryRunner); } else { await this.assetRepository.createAsset( bidDto, - Math.floor(buyData.price * buyData.quantity), + parseFloat((buyData.price * buyData.quantity).toFixed(8)), buyData.quantity, queryRunner, ); } - tradeData.quantity -= buyData.quantity; + tradeData.quantity -= parseFloat(buyData.quantity.toFixed(8)); - if (tradeData.quantity === 0) { + if (tradeData.quantity <= 0.00000001) { await this.tradeRepository.deleteTrade(tradeId, queryRunner); } else await this.tradeRepository.updateTradeTransaction(tradeData, queryRunner); const change = (tradeData.price - buyData.price) * buyData.quantity; - const returnChange = Math.floor(change + account[typeGiven]) + const returnChange = parseFloat((change + account[typeGiven]).toFixed(8)) const new_asset = await this.assetRepository.findOne({ where: {account:{id:account.id}, assetName: "BTC"} }) diff --git a/packages/server/src/trade/trade.entity.ts b/packages/server/src/trade/trade.entity.ts index 8d0fb4aa..f238d427 100644 --- a/packages/server/src/trade/trade.entity.ts +++ b/packages/server/src/trade/trade.entity.ts @@ -30,6 +30,8 @@ export class Trade { @CreateDateColumn({ type: 'timestamp' }) createdAt: Date; - @ManyToOne(() => User, (user) => user.trades) + @ManyToOne(() => User, (user) => user.trades, { + onDelete: 'CASCADE', + }) user: User; } diff --git a/packages/server/src/upbit/chart.repository.ts b/packages/server/src/upbit/chart.repository.ts index 8f2bee54..f9ef2720 100644 --- a/packages/server/src/upbit/chart.repository.ts +++ b/packages/server/src/upbit/chart.repository.ts @@ -30,4 +30,10 @@ export class ChartRepository { console.error("DB Searching Error : "+error) } } + async getSimpleChartData(key){ + const data = await this.chartRedis.get(key); + if(!data){ + return false; + }else return JSON.parse(data) + } } diff --git a/packages/server/src/upbit/chart.service.ts b/packages/server/src/upbit/chart.service.ts index ce27db6c..0944cedf 100644 --- a/packages/server/src/upbit/chart.service.ts +++ b/packages/server/src/upbit/chart.service.ts @@ -18,10 +18,6 @@ export class ChartService implements OnModuleInit{ this.cleanQueue() } async upbitApiDoor(type,coin,to, minute){ - console.log("type : "+type) - console.log("market : "+coin) - console.log("minute : "+minute) - console.log("to : "+to) const validMinutes = ["1", "3", "5", "10", "15", "30", "60", "240"]; if (type === 'minutes') { if (!minute || !validMinutes.includes(minute)) { @@ -201,4 +197,79 @@ export class ChartService implements OnModuleInit{ } setTimeout(()=>this.cleanQueue(),100) } + makeCandle(coinData){ + const name = coinData.code; + // date와 time을 각각 파싱 + const year = coinData.trade_date.slice(0, 4); + const month = coinData.trade_date.slice(4, 6); + const day = coinData.trade_date.slice(6, 8); + + const hour = coinData.trade_time.slice(0, 2); + const minute = coinData.trade_time.slice(2, 4); + const second = coinData.trade_time.slice(4, 6); + + const tradeDate = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}`); + const kstDate = new Date(tradeDate.getTime() + 9 * 60 * 60 * 1000 * 2); + + const price = coinData.trade_price; + const timestamp = coinData.trade_timestamp; + const candle_acc_trade_volume = coinData.trade_volume + const candle_acc_trade_price = price * candle_acc_trade_volume; + const candle = { + market : name, + candle_date_time_utc : kstDate.toISOString().slice(0,19), + candle_date_time_kst : kstDate.toISOString().slice(0,19), + opening_price : price, + high_price : price, + low_price : price, + trade_price : price, + timestamp : timestamp, + candle_acc_trade_price : candle_acc_trade_price, + candle_acc_trade_volume : candle_acc_trade_volume, + prev_closing_price : 0, + change_price : 0, + change_rate : 0, + } + + const type = ['years','months','weeks','days','minutes','seconds']; + const minute_type = ["1", "3", "5", "10", "15", "30", "60", "240"]; + type.forEach(async (key)=>{ + if(key === 'minutes'){ + const keys = []; + minute_type.forEach((min)=>{ + keys.push(this.formatDate(kstDate, key, name, min)); + }) + keys.forEach(async (min)=>{ + const candleData = await this.chartRepository.getSimpleChartData(min); + if(!candleData){ + this.chartRepository.setChartData(min,JSON.stringify(candle)) + }else{ + candleData.trade_price = price; + candleData.high_price = candleData.high_price < price ? price : candleData.high_price; + candleData.low_price = candleData.low_price > price ? price : candleData.low_price; + candleData.timestamp = timestamp; + candleData.candle_acc_trade_price = candle_acc_trade_price; + candleData.candle_acc_trade_volume += candle_acc_trade_volume; + + this.chartRepository.setChartData(min,JSON.stringify(candleData)) + } + }) + }else{ + const redisKey = this.formatDate(kstDate, key, name, null); + const candleData = await this.chartRepository.getSimpleChartData(redisKey); + if(!candleData){ + this.chartRepository.setChartData(redisKey,JSON.stringify(candle)) + }else{ + candleData.trade_price = price; + candleData.high_price = candleData.high_price < price ? price : candleData.high_price; + candleData.low_price = candleData.low_price > price ? price : candleData.low_price; + candleData.timestamp = timestamp; + candleData.candle_acc_trade_price = candle_acc_trade_price; + candleData.candle_acc_trade_volume += candle_acc_trade_volume; + + this.chartRepository.setChartData(redisKey,JSON.stringify(candleData)) + } + } + }) + } } \ No newline at end of file diff --git a/packages/server/src/upbit/coin-list.service.ts b/packages/server/src/upbit/coin-list.service.ts index 2553edea..37c4c3e5 100644 --- a/packages/server/src/upbit/coin-list.service.ts +++ b/packages/server/src/upbit/coin-list.service.ts @@ -34,15 +34,15 @@ export class CoinListService implements OnModuleInit { }); } async getSimpleCoin(coins) { - console.log(coins); + console.log(coins); let krwCoinInfo = this.coinDataUpdaterService.getKrwCoinInfo(); while (!krwCoinInfo) { await new Promise((resolve) => setTimeout(resolve, 100)); krwCoinInfo = this.coinDataUpdaterService.getKrwCoinInfo(); } - if (!coins.length) return []; - + if (!coins.length) return []; + return krwCoinInfo .filter((coin) => coins.includes(coin.market)) .map((coin) => { @@ -82,6 +82,19 @@ export class CoinListService implements OnModuleInit { .filter((coin) => coin.market.startsWith('USDT')); } + getCoinTickers(coins) { + const coinData = this.coinDataUpdaterService.getCoinLatestInfo(); + + const filteredData = Array.from(coinData.entries()) + .filter(([symbol]) => !coins || coins.includes(symbol)) + .map(([symbol, details]) => ({ + code: symbol, + ...details, + })); + + return filteredData; + } + convertToCodeCoinDto = (coin) => { coin.korean_name = this.coinDataUpdaterService .getCoinNameList() diff --git a/packages/server/src/upbit/coin-ticker-websocket.service.ts b/packages/server/src/upbit/coin-ticker-websocket.service.ts index 8af7110a..de8582cd 100644 --- a/packages/server/src/upbit/coin-ticker-websocket.service.ts +++ b/packages/server/src/upbit/coin-ticker-websocket.service.ts @@ -7,6 +7,7 @@ import { UPBIT_WEBSOCKET_CONNECTION_TIME, UPBIT_WEBSOCKET_URL, } from 'common/upbit'; +import { ChartService } from './chart.service'; @Injectable() export class CoinTickerService implements OnModuleInit { @@ -16,6 +17,7 @@ export class CoinTickerService implements OnModuleInit { constructor( private readonly coinListService: CoinListService, private readonly sseService: SseService, + private readonly chartService: ChartService ) {} onModuleInit() { @@ -38,6 +40,7 @@ export class CoinTickerService implements OnModuleInit { const message = JSON.parse(data.toString()); if (message.error) throw new Error(JSON.stringify(message)); this.sseService.coinTickerSendEvent(message); + //this.chartService.makeCandle(message); } catch (error) { console.error('CoinTickerWebSocket 오류:', error); } diff --git a/packages/server/src/upbit/upbit.controller.ts b/packages/server/src/upbit/upbit.controller.ts index cca4c634..422ae9e5 100644 --- a/packages/server/src/upbit/upbit.controller.ts +++ b/packages/server/src/upbit/upbit.controller.ts @@ -8,87 +8,96 @@ import { ApiQuery } from '@nestjs/swagger'; @Controller('upbit') export class UpbitController { - constructor( - private readonly sseService: SseService, - private readonly coinListService: CoinListService, - private readonly chartService : ChartService - ) {} + constructor( + private readonly sseService: SseService, + private readonly coinListService: CoinListService, + private readonly chartService: ChartService, + ) {} - @Sse('price-updates') - priceUpdates(@Query('coins') coins: string[]): Observable { - coins = coins || []; - const initData = this.sseService.initPriceStream( - coins, - this.coinListService.convertToMarketCoinDto, - ); - return concat( - initData, - this.sseService.getPriceUpdatesStream( - coins, - this.coinListService.convertToCodeCoinDto, - ), - ); - } - @Sse('orderbook') - orderbookUpdates(@Query('coins') coins: string[]): Observable { - coins = coins || []; - return this.sseService.getOrderbookUpdatesStream( - coins, - this.coinListService.convertToOrderbookDto, - ); - } + @Sse('price-updates') + priceUpdates(@Query('coins') coins: string[]): Observable { + coins = coins || []; + const initData = this.sseService.initPriceStream( + coins, + this.coinListService.convertToMarketCoinDto, + ); + return concat( + initData, + this.sseService.getPriceUpdatesStream( + coins, + this.coinListService.convertToCodeCoinDto, + ), + ); + } + @Sse('orderbook') + orderbookUpdates(@Query('coins') coins: string[]): Observable { + coins = coins || []; + return this.sseService.getOrderbookUpdatesStream( + coins, + this.coinListService.convertToOrderbookDto, + ); + } - @Get('market/all') - getAllMarkets() { - return this.coinListService.getAllCoinList(); - } - @Get('market/krw') - getKRWMarkets() { - return this.coinListService.getKRWCoinList(); - } - @Get('market/btc') - getBTCMarkets() { - return this.coinListService.getBTCCoinList(); - } - @Get('market/usdt') - getUSDTMarkets() { - return this.coinListService.getUSDTCoinList(); - } + @Get('market/all') + getAllMarkets() { + return this.coinListService.getAllCoinList(); + } + @Get('market/krw') + getKRWMarkets() { + return this.coinListService.getKRWCoinList(); + } + @Get('market/btc') + getBTCMarkets() { + return this.coinListService.getBTCCoinList(); + } + @Get('market/usdt') + getUSDTMarkets() { + return this.coinListService.getUSDTCoinList(); + } - @Get('market/top20-trade/krw') - getTop20TradeKRW() { - return this.coinListService.getMostTradeCoin(); - } - @Get('market/simplelist/krw') - getSomeKRW(@Query('market') market: string[]) { - const marketList = market || []; - return this.coinListService.getSimpleCoin(marketList); - } + @Get('market/top20-trade/krw') + getTop20TradeKRW() { + return this.coinListService.getMostTradeCoin(); + } + @Get('market/simplelist/krw') + getSomeKRW(@Query('market') market: string[]) { + const marketList = market || []; + return this.coinListService.getSimpleCoin(marketList); + } + @Get('market/tickers') + @ApiQuery({ name: 'coins', required: false, type: String }) + getCoinTickers(@Query('coins') coins?: string) { + return this.coinListService.getCoinTickers(coins); + } - @Get('candle/:type/:minute?') - @ApiQuery({ name: 'minute', required: false, type: String }) - async getCandle( - @Res() res: Response, - @Param('type') type : string, - @Query('market') market: string, - @Query('to') to:string, - @Param('minute') minute? :string - ){ - try{ - console.log("type : "+type) - console.log("market : "+market) - console.log("minute : "+minute) - console.log("to : "+to) - const response = await this.chartService.upbitApiDoor(type, market, to, minute) + @Get('candle/:type/:minute?') + @ApiQuery({ name: 'minute', required: false, type: String }) + async getCandle( + @Res() res: Response, + @Param('type') type: string, + @Query('market') market: string, + @Query('to') to: string, + @Param('minute') minute?: string, + ) { + try { + console.log('type : ' + type); + console.log('market : ' + market); + console.log('minute : ' + minute); + console.log('to : ' + to); + const response = await this.chartService.upbitApiDoor( + type, + market, + to, + minute, + ); - return res.status(response.statusCode).json(response) - }catch(error){ - console.error("error"+error) - return res.status(error.status) - .json({ - message: error.message || '서버오류입니다.', - error: error?.response || null, - });; - } - } + return res.status(response.statusCode).json(response); + } catch (error) { + console.error('error' + error); + return res.status(error.status).json({ + message: error.message || '서버오류입니다.', + error: error?.response || null, + }); + } + } }