- {data &&
}
- {data &&
}
+
+ {data && (
+
+ )}
+ {data && }
);
}
diff --git a/src/features/tradeview/ui/StockChart/index.tsx b/src/features/tradeview/ui/StockChart/index.tsx
index 697fa2f..13e5e93 100644
--- a/src/features/tradeview/ui/StockChart/index.tsx
+++ b/src/features/tradeview/ui/StockChart/index.tsx
@@ -15,10 +15,11 @@ import {
useState,
} from 'react';
-import ChartContainer from './ChartContainer';
-import ChartRoot from './ChartRoot';
-import Series from './Series';
-import ToolTip from './ToolTip';
+import ChartContainer from '../../../../shared/ui/Chart/ChartContainer';
+import ChartRoot from '../../../../shared/ui/Chart/ChartRoot';
+import Series from '../../../../shared/ui/Chart/Series';
+import ToolTip from '../../../../shared/ui/Chart/ToolTip';
+import IntervalSelector from '../IntervalSelector';
import api from '../../api/tradeview.endpoints';
import { INTERVALS, MINUTE } from '../../const/chart.const';
@@ -29,7 +30,6 @@ import {
priceFormatter,
timestampToISOString,
} from '../../utils';
-import IntervalSelector from '../IntervalSelector';
type ChartProps = {
ticker?: string;
diff --git a/src/mocks/dummy.ts b/src/mocks/dummy.ts
new file mode 100644
index 0000000..c4726d3
--- /dev/null
+++ b/src/mocks/dummy.ts
@@ -0,0 +1,442 @@
+import type { UserInfoResponseData } from '~/entities/user';
+import {
+ OrderStatus,
+ OrderType,
+ Side,
+ type TradingHistory,
+} from '~/features/profile/types/tradingHistory.type';
+
+export const DUMMY_USERINFO_DATA: UserInfoResponseData = {
+ userId: 1,
+ email: 'test@gmail.com',
+ nickname: 'test',
+ provider: 'test',
+ cash: 100000,
+ totalAssetAmount: 100000,
+ wallets: [
+ {
+ name: 'BTC',
+ ticker: 'BTC',
+ accountId: 1,
+ buyPrice: 100000,
+ currentPrice: 100000,
+ roi: 0.001,
+ size: 1,
+ },
+ {
+ name: 'ETH',
+ ticker: 'ETH',
+ accountId: 2,
+ buyPrice: 200000,
+ currentPrice: 200000,
+ roi: 0.002,
+ size: 1,
+ },
+ {
+ name: 'SOL',
+ ticker: 'SOL',
+ accountId: 3,
+ buyPrice: 40000,
+ currentPrice: 40000,
+ roi: 0.003,
+ size: 1,
+ },
+ {
+ name: 'ADA',
+ ticker: 'ADA',
+ accountId: 4,
+ buyPrice: 100000,
+ currentPrice: 100000,
+ roi: 0.004,
+ size: 1,
+ },
+ {
+ name: 'ATOM',
+ ticker: 'ATOM',
+ accountId: 5,
+ buyPrice: 50000,
+ currentPrice: 50000,
+ roi: -0.005,
+ size: 1,
+ },
+ {
+ name: 'XRP',
+ ticker: 'XRP',
+ accountId: 6,
+ buyPrice: 25000,
+ currentPrice: 25000,
+ roi: 0.006,
+ size: 1,
+ },
+ ],
+};
+
+export const DUMMY_HISTORY_LIST: TradingHistory[] = [
+ {
+ orderId: '1',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'BTC',
+ name: 'BTC',
+ orderSize: 1,
+ price: 100000,
+ orderStatus: OrderStatus.UNSETTLED,
+ remainingSize: 1,
+ displaySize: 1,
+ tradeTime: '2025-07-23T21:02:59.000Z',
+ },
+ {
+ orderId: '2',
+ side: Side.BID,
+ orderType: OrderType.MARKET,
+ ticker: 'ETH',
+ name: 'ETH',
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T21:02:59.000Z',
+ price: 200000,
+ },
+ {
+ orderId: '3',
+ side: Side.ASK,
+ orderType: OrderType.MARKET,
+ ticker: 'XRP',
+ name: 'XRP',
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T21:02:59.000Z',
+ orderSize: 1,
+ remainingSize: 1,
+ displaySize: 1,
+ },
+ {
+ orderId: '4',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'ADA',
+ name: 'Cardano',
+ price: 600,
+ orderSize: 300,
+ remainingSize: 150,
+ displaySize: 300,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:58:30.000Z',
+ },
+ {
+ orderId: '5',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'SOL',
+ name: 'Solana',
+ price: 65000,
+ orderSize: 5,
+ remainingSize: 2,
+ displaySize: 5,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:55:15.000Z',
+ },
+ {
+ orderId: '6',
+ side: Side.BID,
+ orderType: OrderType.MARKET,
+ ticker: 'DOGE',
+ name: 'Dogecoin',
+ price: 100000,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:52:45.000Z',
+ },
+ {
+ orderId: '7',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'MATIC',
+ name: 'Polygon',
+ price: 800,
+ orderSize: 200,
+ remainingSize: 80,
+ displaySize: 200,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T20:50:20.000Z',
+ },
+ {
+ orderId: '8',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'DOT',
+ name: 'Polkadot',
+ price: 8500,
+ orderSize: 15,
+ remainingSize: 5,
+ displaySize: 15,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:48:10.000Z',
+ },
+ {
+ orderId: '9',
+ side: Side.ASK,
+ orderType: OrderType.MARKET,
+ ticker: 'LINK',
+ name: 'Chainlink',
+ orderSize: 1,
+ remainingSize: 1,
+ displaySize: 1,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:45:35.000Z',
+ },
+ {
+ orderId: '10',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'AVAX',
+ name: 'Avalanche',
+ price: 18000,
+ orderSize: 25,
+ remainingSize: 10,
+ displaySize: 25,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T20:42:50.000Z',
+ },
+ {
+ orderId: '11',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'LTC',
+ name: 'Litecoin',
+ price: 70000,
+ orderSize: 8,
+ remainingSize: 3,
+ displaySize: 8,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:40:25.000Z',
+ },
+ {
+ orderId: '12',
+ side: Side.BID,
+ orderType: OrderType.MARKET,
+ ticker: 'BNB',
+ name: 'Binance Coin',
+ price: 100000,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:38:15.000Z',
+ },
+ {
+ orderId: '13',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'ATOM',
+ name: 'Cosmos',
+ price: 8400,
+ orderSize: 30,
+ remainingSize: 12,
+ displaySize: 30,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:35:40.000Z',
+ },
+ {
+ orderId: '14',
+ side: Side.BID,
+ orderType: OrderType.MARKET,
+ ticker: 'SHIB',
+ name: 'Shiba Inu',
+ price: 100000,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:33:20.000Z',
+ },
+ {
+ orderId: '15',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'XLM',
+ name: 'Stellar',
+ price: 200,
+ orderSize: 500,
+ remainingSize: 200,
+ displaySize: 500,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T20:30:55.000Z',
+ },
+ {
+ orderId: '16',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'UNI',
+ name: 'Uniswap',
+ price: 6500,
+ orderSize: 40,
+ remainingSize: 15,
+ displaySize: 40,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:28:30.000Z',
+ },
+ {
+ orderId: '17',
+ side: Side.ASK,
+ orderType: OrderType.MARKET,
+ ticker: 'SAND',
+ name: 'The Sandbox',
+ orderSize: 1,
+ remainingSize: 1,
+ displaySize: 1,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:26:15.000Z',
+ },
+ {
+ orderId: '18',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'MANA',
+ name: 'Decentraland',
+ price: 380,
+ orderSize: 100,
+ remainingSize: 45,
+ displaySize: 100,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T20:24:00.000Z',
+ },
+ {
+ orderId: '19',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'ALGO',
+ name: 'Algorand',
+ price: 150,
+ orderSize: 800,
+ remainingSize: 300,
+ displaySize: 800,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:21:45.000Z',
+ },
+ {
+ orderId: '20',
+ side: Side.BID,
+ orderType: OrderType.MARKET,
+ ticker: 'FTM',
+ name: 'Fantom',
+ price: 250,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:19:30.000Z',
+ },
+ {
+ orderId: '21',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'NEAR',
+ name: 'NEAR Protocol',
+ price: 2800,
+ orderSize: 60,
+ remainingSize: 25,
+ displaySize: 60,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T20:17:10.000Z',
+ },
+ {
+ orderId: '22',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'HBAR',
+ name: 'Hedera',
+ price: 50,
+ orderSize: 2000,
+ remainingSize: 800,
+ displaySize: 2000,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:14:55.000Z',
+ },
+ {
+ orderId: '23',
+ side: Side.ASK,
+ orderType: OrderType.MARKET,
+ ticker: 'VET',
+ name: 'VeChain',
+ orderSize: 1,
+ remainingSize: 1,
+ displaySize: 1,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:12:35.000Z',
+ },
+ {
+ orderId: '24',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'ICP',
+ name: 'Internet Computer',
+ price: 4200,
+ orderSize: 12,
+ remainingSize: 4,
+ displaySize: 12,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T20:10:20.000Z',
+ },
+ {
+ orderId: '25',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'FLOW',
+ name: 'Flow',
+ price: 650,
+ orderSize: 150,
+ remainingSize: 60,
+ displaySize: 150,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:08:05.000Z',
+ },
+ {
+ orderId: '26',
+ side: Side.BID,
+ orderType: OrderType.MARKET,
+ ticker: 'THETA',
+ name: 'Theta Network',
+ price: 100000,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T20:05:50.000Z',
+ },
+ {
+ orderId: '27',
+ side: Side.ASK,
+ orderType: OrderType.LIMIT,
+ ticker: 'EGLD',
+ name: 'MultiversX',
+ price: 15000,
+ orderSize: 6,
+ remainingSize: 2,
+ displaySize: 6,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T20:03:30.000Z',
+ },
+ {
+ orderId: '28',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'XTZ',
+ name: 'Tezos',
+ price: 900,
+ orderSize: 90,
+ remainingSize: 35,
+ displaySize: 90,
+ orderStatus: OrderStatus.UNSETTLED,
+ tradeTime: '2025-07-23T20:01:15.000Z',
+ },
+ {
+ orderId: '29',
+ side: Side.ASK,
+ orderType: OrderType.MARKET,
+ ticker: 'AAVE',
+ name: 'Aave',
+ orderSize: 1,
+ remainingSize: 1,
+ displaySize: 1,
+ orderStatus: OrderStatus.SETTLED,
+ tradeTime: '2025-07-23T19:59:00.000Z',
+ },
+ {
+ orderId: '30',
+ side: Side.BID,
+ orderType: OrderType.LIMIT,
+ ticker: 'CRV',
+ name: 'Curve DAO Token',
+ price: 320,
+ orderSize: 200,
+ remainingSize: 75,
+ displaySize: 200,
+ orderStatus: OrderStatus.IN_PROGRESS,
+ tradeTime: '2025-07-23T19:56:45.000Z',
+ },
+];
diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts
index a422d26..7d7734e 100644
--- a/src/mocks/handlers.ts
+++ b/src/mocks/handlers.ts
@@ -1,12 +1,88 @@
/* v8 ignore start */
import { http, HttpResponse } from 'msw';
+import {
+ type HistoryResponseData,
+ OrderStatus,
+} from '~/features/profile/types/tradingHistory.type';
+import type { Response } from '~/shared/types/api';
+import { DUMMY_HISTORY_LIST, DUMMY_USERINFO_DATA } from './dummy';
+
+let historyList = [...DUMMY_HISTORY_LIST];
+
+function api(endpoint: string) {
+ return `http://localhost:8080/api/${endpoint}`;
+}
+
+function successResponse
(data: T) {
+ const response: Response = {
+ data: data,
+ isSuccess: true,
+ error: null,
+ };
+
+ return response;
+}
export const handlers = [
- http.get('/api/tokencheck', async ({ cookies }) => {
+ http.get(api('tokencheck'), async ({ cookies }) => {
if (!cookies.access_token) {
return new HttpResponse(null, { status: 401 });
}
return new HttpResponse(null, { status: 200 });
}),
+ http.get(api('userinfo'), async () => {
+ return HttpResponse.json(successResponse(DUMMY_USERINFO_DATA), {
+ status: 200,
+ });
+ }),
+ http.get(api('userinfo/trades'), async ({ request }) => {
+ const { searchParams } = new URL(request.url);
+ const page = Number(searchParams.get('page') || 1);
+ const size = Number(searchParams.get('size') || 10);
+ const settled = searchParams.get('settled') === 'true';
+
+ const filteredOrderlist = historyList.filter((item) =>
+ settled
+ ? item.orderStatus === OrderStatus.SETTLED
+ : item.orderStatus !== OrderStatus.SETTLED,
+ );
+
+ const firstItemIndex = (page - 1) * size;
+ const lastItemIndex = page * size - 1;
+
+ const historyData: HistoryResponseData = {
+ orderList: filteredOrderlist.slice(firstItemIndex, lastItemIndex + 1),
+ totalPages: Math.ceil(filteredOrderlist.length / size),
+ currentPage: page,
+ pageSize: size,
+ totalElements: filteredOrderlist.length,
+ };
+
+ if (page < 1 || size < 1 || firstItemIndex < 0) {
+ return HttpResponse.json('잘못된 요청입니다.', {
+ status: 400,
+ });
+ }
+
+ return HttpResponse.json(successResponse(historyData), {
+ status: 200,
+ });
+ }),
+ http.delete(api('userinfo/trades'), async ({ request }) => {
+ const { searchParams } = new URL(request.url);
+ const orderId = searchParams.get('orderId');
+
+ if (!orderId) {
+ return HttpResponse.json('잘못된 요청입니다.', {
+ status: 400,
+ });
+ }
+
+ historyList = historyList.filter((item) => item.orderId !== orderId);
+
+ return HttpResponse.json(successResponse(null), {
+ status: 205,
+ });
+ }),
];
/* v8 ignore end */
diff --git a/src/shared/hooks/hooks.test.tsx b/src/shared/hooks/hooks.test.tsx
index 9ee6cce..acfc96c 100644
--- a/src/shared/hooks/hooks.test.tsx
+++ b/src/shared/hooks/hooks.test.tsx
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
import useClickOutside from './useClickOutside';
import useDimensions from './useDimensions';
-import useScrollToBottom from './useScrollToBottom';
+import useScrollIntoView from './useScrollIntoView';
describe('useClickOutside 훅 테스트', () => {
it('ref가 부착된 컴포넌트가 아닌 바깥 컴포넌트를 클릭하면 callback함수가 실행된다.', () => {
@@ -50,8 +50,10 @@ describe('useScrollToBottom 훅 테스트', () => {
const mockScrollIntoView = vi.fn();
const { result, rerender } = renderHook(
- ({ deps }) => useScrollToBottom(deps),
- { initialProps: { deps: [1] } },
+ ({ deps }) => useScrollIntoView(deps),
+ {
+ initialProps: { deps: [1] },
+ },
);
act(() => {
diff --git a/src/shared/hooks/useCustomReferer.tsx b/src/shared/hooks/useCustomReferer.tsx
new file mode 100644
index 0000000..69e2f81
--- /dev/null
+++ b/src/shared/hooks/useCustomReferer.tsx
@@ -0,0 +1,7 @@
+import { useSearchParams } from 'react-router';
+
+export default function useCustomReferer() {
+ const [searchParams] = useSearchParams();
+
+ return searchParams.get('referer');
+}
diff --git a/src/shared/hooks/useScrollToBottom.tsx b/src/shared/hooks/useScrollIntoView.tsx
similarity index 56%
rename from src/shared/hooks/useScrollToBottom.tsx
rename to src/shared/hooks/useScrollIntoView.tsx
index 4c40eba..4ded5c6 100644
--- a/src/shared/hooks/useScrollToBottom.tsx
+++ b/src/shared/hooks/useScrollIntoView.tsx
@@ -1,19 +1,16 @@
import { useEffect, useRef } from 'react';
import type { DependencyList } from 'react';
-export default function useScrollToBottom<
+export default function useScrollIntoView<
T extends HTMLElement = HTMLDivElement,
->(dependencies: DependencyList = []) {
+>(dependencies: DependencyList = [], options?: ScrollIntoViewOptions) {
const bottomElementRef = useRef(null);
useEffect(() => {
if (!bottomElementRef.current) return;
- bottomElementRef.current.scrollIntoView({
- behavior: 'smooth',
- block: 'end',
- });
- }, dependencies);
+ bottomElementRef.current.scrollIntoView(options);
+ }, [...dependencies, options]);
return bottomElementRef;
}
diff --git a/src/shared/hooks/useScrollTo.tsx b/src/shared/hooks/useScrollTo.tsx
new file mode 100644
index 0000000..d040a40
--- /dev/null
+++ b/src/shared/hooks/useScrollTo.tsx
@@ -0,0 +1,18 @@
+import { type DependencyList, useEffect, useRef } from 'react';
+
+export default function useScrollTo(
+ dependencies: DependencyList = [],
+ options?: ScrollToOptions,
+) {
+ const scrollContainerRef = useRef(null);
+
+ useEffect(() => {
+ const scrollContainer = scrollContainerRef.current;
+
+ if (!scrollContainer) return;
+
+ scrollContainer.scrollTo(options);
+ }, [...dependencies, options]);
+
+ return scrollContainerRef;
+}
diff --git a/src/shared/ui/Button/index.tsx b/src/shared/ui/Button/index.tsx
index f7874d7..549edeb 100644
--- a/src/shared/ui/Button/index.tsx
+++ b/src/shared/ui/Button/index.tsx
@@ -1,13 +1,24 @@
+import clsx from 'clsx';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
export type ButtonProps = {
children: ReactNode;
+ buttonStyle?: 'primary' | 'secondary' | 'warn';
} & ButtonHTMLAttributes;
-export default function Button({ children, ...props }: ButtonProps) {
+export default function Button({
+ children,
+ buttonStyle = 'primary',
+ ...props
+}: ButtonProps) {
return (
{children}
diff --git a/src/features/tradeview/ui/StockChart/ChartContainer.tsx b/src/shared/ui/Chart/ChartContainer.tsx
similarity index 96%
rename from src/features/tradeview/ui/StockChart/ChartContainer.tsx
rename to src/shared/ui/Chart/ChartContainer.tsx
index 1671441..2eaf2ba 100644
--- a/src/features/tradeview/ui/StockChart/ChartContainer.tsx
+++ b/src/shared/ui/Chart/ChartContainer.tsx
@@ -17,7 +17,7 @@ import {
useRef,
} from 'react';
-import { INTERVAL_SELECTOR_HEIGHT } from '../../const/chart.const';
+import { INTERVAL_SELECTOR_HEIGHT } from '../../../features/tradeview/const/chart.const';
import { useChartRoot } from './ChartRoot';
type ChartContainerProps = PropsWithChildren<{
diff --git a/src/features/tradeview/ui/StockChart/ChartRoot.tsx b/src/shared/ui/Chart/ChartRoot.tsx
similarity index 100%
rename from src/features/tradeview/ui/StockChart/ChartRoot.tsx
rename to src/shared/ui/Chart/ChartRoot.tsx
diff --git a/src/features/tradeview/ui/StockChart/Series.tsx b/src/shared/ui/Chart/Series.tsx
similarity index 100%
rename from src/features/tradeview/ui/StockChart/Series.tsx
rename to src/shared/ui/Chart/Series.tsx
diff --git a/src/features/tradeview/ui/StockChart/ToolTip.tsx b/src/shared/ui/Chart/ToolTip.tsx
similarity index 91%
rename from src/features/tradeview/ui/StockChart/ToolTip.tsx
rename to src/shared/ui/Chart/ToolTip.tsx
index 7ea64dc..9e29cf0 100644
--- a/src/features/tradeview/ui/StockChart/ToolTip.tsx
+++ b/src/shared/ui/Chart/ToolTip.tsx
@@ -1,11 +1,14 @@
import type { CandlestickData } from 'lightweight-charts';
import { useLayoutEffect, useRef } from 'react';
import { formatCurrencyKR } from '~/shared/utils';
-import { formatDateKr } from '../../utils';
+import { formatDateKr } from '../../../features/tradeview/utils';
import { useChartContainer } from './ChartContainer';
import { useChartRoot } from './ChartRoot';
import { useSeries } from './Series';
+const HOUR = 60 * 60 * 1000;
+const TIME_OFFSET = 9;
+
const TOOLTIP_WIDTH = 80;
const TOOLTIP_HEIGHT = 80;
const TOOLTIP_MARGIN = 15;
@@ -38,7 +41,7 @@ export default function ToolTip() {
chartSeries,
) as CandlestickData;
const date = new Date((time as number) * 1000);
- const koreanDate = new Date(date.setHours(date.getHours() - 9));
+ const koreanDate = new Date(date.getTime() + HOUR * TIME_OFFSET);
toolTipElementRef.current.style.display = 'block';
toolTipElementRef.current.innerHTML = `
@@ -77,7 +80,7 @@ export default function ToolTip() {
);
}
diff --git a/src/shared/ui/ClientOnly/index.tsx b/src/shared/ui/ClientOnly/index.tsx
new file mode 100644
index 0000000..edfdb78
--- /dev/null
+++ b/src/shared/ui/ClientOnly/index.tsx
@@ -0,0 +1,11 @@
+type ClientOnlyProps = {
+ children: React.ReactNode;
+ fallback?: React.ReactNode;
+};
+
+export default function ClientOnly({
+ children,
+ fallback = null,
+}: ClientOnlyProps) {
+ return typeof window !== 'undefined' ? <>{children}> : fallback;
+}
diff --git a/src/shared/ui/Error/index.tsx b/src/shared/ui/Error/index.tsx
new file mode 100644
index 0000000..e476b73
--- /dev/null
+++ b/src/shared/ui/Error/index.tsx
@@ -0,0 +1,37 @@
+import Lottie from 'lottie-react';
+import { useNavigate } from 'react-router';
+
+import ErrorAnimation from '~/assets/lotties/error.json';
+import Button from '../Button';
+import ClientOnly from '../ClientOnly';
+
+export type ErrorComponentProps = {
+ title: string;
+ description: string;
+};
+
+export default function ErrorComponent({
+ title,
+ description,
+}: ErrorComponentProps) {
+ const navigate = useNavigate();
+
+ const handleGoBack = () => {
+ navigate(-1);
+ };
+
+ return (
+
+
+
+
+
{title}
+
{description}
+
+
+ 뒤로 가기
+
+
+
+ );
+}
diff --git a/src/shared/ui/ErrorModal/index.tsx b/src/shared/ui/ErrorModal/index.tsx
new file mode 100644
index 0000000..66fa04b
--- /dev/null
+++ b/src/shared/ui/ErrorModal/index.tsx
@@ -0,0 +1,28 @@
+import { useRef } from 'react';
+import { useNavigate } from 'react-router';
+
+import useClickOutside from '~/shared/hooks/useClickOutside';
+import useCustomReferer from '~/shared/hooks/useCustomReferer';
+import Backdrop from '~/shared/ui/Backdrop';
+import ErrorComponent, { type ErrorComponentProps } from '~/shared/ui/Error';
+import Modal from '~/shared/ui/Modal';
+
+type ErrorModalProps = ErrorComponentProps;
+
+export default function ErrorModal({ title, description }: ErrorModalProps) {
+ const referer = useCustomReferer();
+ const navigate = useNavigate();
+ const modalRef = useRef
(null);
+
+ useClickOutside(modalRef, () => navigate(referer || '/trade/BTC'));
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/shared/ui/IncrementingNumber/index.tsx b/src/shared/ui/IncrementingNumber/index.tsx
new file mode 100644
index 0000000..a8f7993
--- /dev/null
+++ b/src/shared/ui/IncrementingNumber/index.tsx
@@ -0,0 +1,39 @@
+import { animate, motion, useMotionValue, useTransform } from 'motion/react';
+import { useEffect } from 'react';
+import { formatCurrencyKR } from '~/shared/utils';
+
+type IncrementingNumberProps = {
+ children: number | string;
+ formatToCurrencyKr?: boolean;
+ duration?: number;
+};
+
+export default function IncrementingNumber({
+ children,
+ formatToCurrencyKr = false,
+ duration = 1,
+}: IncrementingNumberProps) {
+ const number = Number(children);
+
+ if (typeof children !== 'number' || Number.isNaN(number)) {
+ throw new Error('children must be a number');
+ }
+
+ const value = useMotionValue(0);
+ const rounded = useTransform(() =>
+ formatToCurrencyKr
+ ? formatCurrencyKR(Math.round(value.get()))
+ : String(Math.round(value.get())),
+ );
+
+ useEffect(() => {
+ const control = animate(value, number, {
+ duration,
+ ease: 'easeOut',
+ });
+
+ return () => control.stop();
+ }, [number, value, duration]);
+
+ return {rounded} ;
+}
diff --git a/src/shared/ui/Modal/index.tsx b/src/shared/ui/Modal/index.tsx
index ba47450..acccb07 100644
--- a/src/shared/ui/Modal/index.tsx
+++ b/src/shared/ui/Modal/index.tsx
@@ -10,7 +10,7 @@ export default function Modal({ children, ref }: Readonly) {
{children}
diff --git a/src/shared/ui/NoContent/index.tsx b/src/shared/ui/NoContent/index.tsx
new file mode 100644
index 0000000..7b15950
--- /dev/null
+++ b/src/shared/ui/NoContent/index.tsx
@@ -0,0 +1,35 @@
+import Lottie from 'lottie-react';
+import type { CSSProperties } from 'react';
+
+import NoContentAnimation from '~/assets/lotties/no-content.json';
+import ClientOnly from '../ClientOnly';
+
+export type NoContentProps = {
+ title: string;
+ description?: string;
+ style?: CSSProperties;
+};
+
+export default function NoContent({
+ title,
+ description,
+ style,
+}: NoContentProps) {
+ return (
+
+
+
+
+
{title}
+ {description &&
{description}
}
+
+ );
+}
diff --git a/src/shared/ui/Pagination/index.tsx b/src/shared/ui/Pagination/index.tsx
new file mode 100644
index 0000000..ecfad34
--- /dev/null
+++ b/src/shared/ui/Pagination/index.tsx
@@ -0,0 +1,70 @@
+import clsx from 'clsx';
+import type { MouseEvent } from 'react';
+import { IconArrowLeft, IconArrowRight } from '~/assets/svgs';
+
+type PaginationProps = {
+ currentPage: number;
+ totalPages: number;
+ showCount: number;
+ onClick: (page: number) => void;
+ onPrevClick: () => void;
+ onNextClick: () => void;
+};
+
+export default function Pagination({
+ currentPage,
+ totalPages,
+ showCount,
+ onClick,
+ onPrevClick,
+ onNextClick,
+}: Readonly) {
+ const pages: number[] = [];
+
+ const currentSection = Math.ceil(currentPage / showCount);
+ const sectionStart = (currentSection - 1) * showCount + 1;
+ const sectionEnd = Math.min(currentSection * showCount, totalPages);
+
+ for (let page = sectionStart; page <= sectionEnd; page++) {
+ pages.push(page);
+ }
+
+ const handleClick = (e: MouseEvent) => {
+ onClick(Number(e.currentTarget.value));
+ };
+
+ return (
+
+
+
+
+ {pages.map((page) => (
+
+ {page}
+
+ ))}
+ = totalPages}
+ >
+
+
+
+ );
+}
diff --git a/src/shared/ui/Spinner/index.tsx b/src/shared/ui/Spinner/index.tsx
new file mode 100644
index 0000000..ec64c98
--- /dev/null
+++ b/src/shared/ui/Spinner/index.tsx
@@ -0,0 +1,10 @@
+import type { CSSProperties } from 'react';
+import classes from './spinner.module.css';
+
+type SpinnerProps = {
+ style?: CSSProperties;
+};
+
+export default function Spinner({ style }: SpinnerProps) {
+ return
;
+}
diff --git a/src/shared/ui/Spinner/spinner.module.css b/src/shared/ui/Spinner/spinner.module.css
new file mode 100644
index 0000000..6aaf8ad
--- /dev/null
+++ b/src/shared/ui/Spinner/spinner.module.css
@@ -0,0 +1,19 @@
+.loader {
+ width: 16px;
+ padding: 4px;
+ margin: 0 auto;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ background: #2b7fff;
+ --_m: conic-gradient(#0000 10%, #000), linear-gradient(#000 0 0) content-box;
+ -webkit-mask: var(--_m);
+ mask: var(--_m);
+ -webkit-mask-composite: source-out;
+ mask-composite: subtract;
+ animation: l3 1s infinite linear;
+}
+@keyframes l3 {
+ to {
+ transform: rotate(1turn);
+ }
+}
diff --git a/src/shared/ui/Tab/index.tsx b/src/shared/ui/Tab/index.tsx
new file mode 100644
index 0000000..c72ad3d
--- /dev/null
+++ b/src/shared/ui/Tab/index.tsx
@@ -0,0 +1,54 @@
+import { motion } from 'motion/react';
+import { Fragment, type MouseEvent } from 'react';
+
+type TabItem = {
+ value: string;
+ label: string;
+};
+
+type TabProps = {
+ items: TabItem[];
+ selected: TabItem['value'];
+ onClick?: (value: TabItem['value']) => void;
+};
+
+export default function Tab({ items, selected, onClick }: Readonly) {
+ const handleTabClick = (e: MouseEvent) => {
+ const value = e.currentTarget.value;
+ onClick?.(value);
+ };
+
+ return (
+
+
+ {items.map((item, index) => (
+
+
+
+ {item.label}
+
+ {selected === item.value ? (
+
+ ) : (
+
+ )}
+
+ {index < items.length - 1 && (
+
+
+
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts
index d38b5c0..189514f 100644
--- a/src/shared/utils/index.ts
+++ b/src/shared/utils/index.ts
@@ -36,3 +36,13 @@ export function preventNonNumericInput(event: React.KeyboardEvent): void {
export function isNullish(value: unknown): value is null | undefined {
return value === null || value === undefined;
}
+
+export function getCustomReferer(url: string | URL) {
+ const { searchParams } = new URL(url);
+
+ return searchParams.get('referer');
+}
+
+export function convertBase64ToSvg(base64: string) {
+ return `data:image/svg+xml;base64,${base64}`;
+}
diff --git a/src/shared/utils/util.server.ts b/src/shared/utils/util.server.ts
new file mode 100644
index 0000000..f3f71f8
--- /dev/null
+++ b/src/shared/utils/util.server.ts
@@ -0,0 +1,13 @@
+import * as cookie from 'cookie';
+
+export function extractAccessToken(rawCookie: string | null) {
+ if (!rawCookie) return;
+
+ const parsedCookie = cookie.parse(rawCookie);
+ return parsedCookie.access_token;
+}
+
+export function checkLogin(rawCookie: string | null) {
+ const accessToken = extractAccessToken(rawCookie);
+ return !!accessToken;
+}
diff --git a/src/widgets/auth/ui/LoginModal/index.tsx b/src/widgets/auth/ui/LoginModal/index.tsx
index 724e1d3..3cfa3da 100644
--- a/src/widgets/auth/ui/LoginModal/index.tsx
+++ b/src/widgets/auth/ui/LoginModal/index.tsx
@@ -8,10 +8,14 @@ import Modal from '~/shared/ui/Modal';
import CloudLogo from '~/assets/images/cloud.webp';
-export default function LoginModal() {
+type LoginModalProps = {
+ referer: string;
+};
+
+export default function LoginModal({ referer }: LoginModalProps) {
const navigate = useNavigate();
const modalRef = useRef(null);
- useClickOutside(modalRef, () => navigate(-1));
+ useClickOutside(modalRef, () => navigate(referer));
return (
diff --git a/src/widgets/navbar/ui/NavBar/index.tsx b/src/widgets/navbar/ui/NavBar/index.tsx
index 9459ea7..fb18d1f 100644
--- a/src/widgets/navbar/ui/NavBar/index.tsx
+++ b/src/widgets/navbar/ui/NavBar/index.tsx
@@ -1,4 +1,10 @@
-import { Link, type LinkProps, NavLink, useSubmit } from 'react-router';
+import {
+ Link,
+ type LinkProps,
+ NavLink,
+ useLocation,
+ useSubmit,
+} from 'react-router';
import { useUserId } from '~/app/provider/UserInfoProvider';
import type { CoinTicker } from '~/entities/coin';
@@ -23,6 +29,7 @@ export default function NavBar({
ticker,
onClickMenuButton,
}: NavBarProps) {
+ const location = useLocation();
const submit = useSubmit();
const { setUserId } = useUserId();
@@ -32,12 +39,24 @@ export default function NavBar({
};
const LoginButton = () => (
-
+
로그인
);
- const LogoutButton = () => 로그아웃 ;
+ const LogoutButton = () => (
+
+ 로그아웃
+
+ );
+
+ const ProfileButton = () => (
+
+ 프로필
+
+ );
return (
<>
@@ -48,7 +67,10 @@ export default function NavBar({
- {isLoggedIn ? : }
+
+ {isLoggedIn ?
: null}
+ {isLoggedIn ?
:
}
+
>
diff --git a/src/widgets/user/index.ts b/src/widgets/user/index.ts
new file mode 100644
index 0000000..1072635
--- /dev/null
+++ b/src/widgets/user/index.ts
@@ -0,0 +1 @@
+export { default as ProfileModal } from './ui/ProfileModal';
diff --git a/src/widgets/user/ui/ProfileModal/index.tsx b/src/widgets/user/ui/ProfileModal/index.tsx
new file mode 100644
index 0000000..741ad3f
--- /dev/null
+++ b/src/widgets/user/ui/ProfileModal/index.tsx
@@ -0,0 +1,34 @@
+import { useRef } from 'react';
+import { Outlet, useNavigate } from 'react-router';
+
+import type { UserInfoResponseData } from '~/entities/user';
+import AssetInfoGraphic from '~/features/profile/ui/AssetInfoGraphic';
+import useClickOutside from '~/shared/hooks/useClickOutside';
+import useCustomReferer from '~/shared/hooks/useCustomReferer';
+import Backdrop from '~/shared/ui/Backdrop';
+import Modal from '~/shared/ui/Modal';
+
+type ProfileModalProps = {
+ userInfo: UserInfoResponseData;
+};
+
+export default function ProfileModal({ userInfo }: ProfileModalProps) {
+ const referer = useCustomReferer();
+ const navigate = useNavigate();
+ const modalRef = useRef(null);
+
+ useClickOutside(modalRef, () => navigate(referer || '/trade/BTC'));
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/vite.config.ts b/vite.config.ts
index 1de1612..c3b2740 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,21 +1,18 @@
import { reactRouter } from '@react-router/dev/vite';
-import {
- type SentryReactRouterBuildOptions,
- sentryReactRouter,
-} from '@sentry/react-router';
import svgr from '@svgr/rollup';
import tailwindcss from '@tailwindcss/vite';
import { visualizer } from 'rollup-plugin-visualizer';
-import { type PluginOption, defineConfig, loadEnv } from 'vite';
+import { type PluginOption, defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
+/* Sentry 설정 제외 */
export default defineConfig((config) => {
- const env = loadEnv(config.mode, process.cwd());
- const sentryConfig: SentryReactRouterBuildOptions = {
- org: env.VITE_SENTRY_ORG,
- project: env.VITE_SENTRY_PROJECT,
- authToken: env.VITE_SENTRY_AUTH_TOKEN,
- };
+ // const env = loadEnv(config.mode, process.cwd());
+ // const sentryConfig: SentryReactRouterBuildOptions = {
+ // org: env.VITE_SENTRY_ORG,
+ // project: env.VITE_SENTRY_PROJECT,
+ // authToken: env.VITE_SENTRY_AUTH_TOKEN,
+ // };
return {
plugins: [
@@ -24,7 +21,7 @@ export default defineConfig((config) => {
reactRouter(),
tsconfigPaths(),
visualizer() as PluginOption,
- sentryReactRouter(sentryConfig, config),
+ // sentryReactRouter(sentryConfig, config),
],
optimizeDeps: {
exclude: ['@amcharts/amcharts5'],
diff --git a/yarn.lock b/yarn.lock
index cf2ffed..edbb75e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1828,6 +1828,18 @@
morgan "^1.10.0"
source-map-support "^0.5.21"
+"@reduxjs/toolkit@1.x.x || 2.x.x":
+ version "2.8.2"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.8.2.tgz#f4e9f973c6fc930c1e0f3bf462cc95210c28f5f9"
+ integrity sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==
+ dependencies:
+ "@standard-schema/spec" "^1.0.0"
+ "@standard-schema/utils" "^0.3.0"
+ immer "^10.0.3"
+ redux "^5.0.1"
+ redux-thunk "^3.1.0"
+ reselect "^5.1.0"
+
"@rollup/pluginutils@^5.0.2", "@rollup/pluginutils@^5.1.3":
version "5.1.4"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a"
@@ -2214,6 +2226,16 @@
"@sentry/bundler-plugin-core" "3.5.0"
unplugin "1.0.1"
+"@standard-schema/spec@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
+ integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==
+
+"@standard-schema/utils@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
+ integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
+
"@stomp/stompjs@^7.1.1":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-7.1.1.tgz#9a836da33bed5b76c72a8f17f0594de98120f6d6"
@@ -2498,7 +2520,7 @@
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
-"@types/d3-array@*":
+"@types/d3-array@*", "@types/d3-array@^3.0.3":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
@@ -2557,7 +2579,7 @@
resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17"
integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==
-"@types/d3-ease@*":
+"@types/d3-ease@*", "@types/d3-ease@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
@@ -2596,7 +2618,7 @@
resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz#cd4656f5d17a98e26ed5d6f4be96dbda454af8b3"
integrity sha512-QwjxA3+YCKH3N1Rs3uSiSy1bdxlLB1uUiENXeJudBoAFvtDuswUxLcanoOaR2JYn1melDTuIXR8VhnVyI3yG/A==
-"@types/d3-interpolate@*":
+"@types/d3-interpolate@*", "@types/d3-interpolate@^3.0.1":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
@@ -2640,7 +2662,7 @@
resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39"
integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==
-"@types/d3-scale@*":
+"@types/d3-scale@*", "@types/d3-scale@^4.0.2":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb"
integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==
@@ -2652,7 +2674,7 @@
resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3"
integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==
-"@types/d3-shape@*", "@types/d3-shape@^3.0.0":
+"@types/d3-shape@*", "@types/d3-shape@^3.0.0", "@types/d3-shape@^3.1.0":
version "3.1.7"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555"
integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==
@@ -2671,12 +2693,12 @@
resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2"
integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==
-"@types/d3-time@*":
+"@types/d3-time@*", "@types/d3-time@^3.0.0":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f"
integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
-"@types/d3-timer@*":
+"@types/d3-timer@*", "@types/d3-timer@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
@@ -2832,6 +2854,11 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
+"@types/use-sync-external-store@^0.0.6":
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
+ integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
+
"@vitest/coverage-v8@^3.1.4":
version "3.1.4"
resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz#faffd0d22795938b69aa4fedc78622bce299ec26"
@@ -3481,7 +3508,7 @@ csstype@^3.0.2:
dependencies:
internmap "^1.0.0"
-"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.1.6, d3-array@^3.2.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
@@ -3557,7 +3584,7 @@ d3-dispatch@2.*:
iconv-lite "0.6"
rw "1"
-"d3-ease@1 - 3", d3-ease@3:
+"d3-ease@1 - 3", d3-ease@3, d3-ease@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
@@ -3595,7 +3622,7 @@ d3-hierarchy@3, d3-hierarchy@^3.0.0:
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
-"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3, d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
@@ -3648,7 +3675,7 @@ d3-scale-chromatic@3:
d3-color "1 - 3"
d3-interpolate "1 - 3"
-d3-scale@4:
+d3-scale@4, d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
@@ -3664,7 +3691,7 @@ d3-scale@4:
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
-d3-shape@3, d3-shape@^3.0.0:
+d3-shape@3, d3-shape@^3.0.0, d3-shape@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
@@ -3685,14 +3712,14 @@ d3-shape@^1.2.0:
dependencies:
d3-time "1 - 3"
-"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3, d3-time@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
-"d3-timer@1 - 3", d3-timer@3:
+"d3-timer@1 - 3", d3-timer@3, d3-timer@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
@@ -3824,6 +3851,11 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.4.0:
dependencies:
ms "^2.1.3"
+decimal.js-light@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
+ integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
decimal.js@^10.5.0:
version "10.5.0"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22"
@@ -4070,6 +4102,11 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
dependencies:
es-errors "^1.3.0"
+es-toolkit@^1.39.3:
+ version "1.39.5"
+ resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.39.5.tgz#ee2a78a66aafb76c7345af0ea8c06722c78ef1fd"
+ integrity sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==
+
esbuild@^0.25.0:
version "0.25.4"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.4.tgz#bb9a16334d4ef2c33c7301a924b8b863351a0854"
@@ -4545,6 +4582,11 @@ iconv-lite@0.6, iconv-lite@0.6.3, iconv-lite@^0.6.3:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
+immer@^10.0.3, immer@^10.1.1:
+ version "10.1.1"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
+ integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
+
import-fresh@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
@@ -4979,6 +5021,18 @@ log-update@^6.1.0:
strip-ansi "^7.1.0"
wrap-ansi "^9.0.0"
+lottie-react@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/lottie-react/-/lottie-react-2.4.1.tgz#4bd3f2a8a5e48edbd43c05ca5080fdd50f049d31"
+ integrity sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==
+ dependencies:
+ lottie-web "^5.10.2"
+
+lottie-web@^5.10.2:
+ version "5.13.0"
+ resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.13.0.tgz#441d3df217cc8ba302338c3f168e1a3af0f221d3"
+ integrity sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==
+
loupe@^3.1.0, loupe@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2"
@@ -5731,6 +5785,14 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+"react-redux@8.x.x || 9.x.x":
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5"
+ integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==
+ dependencies:
+ "@types/use-sync-external-store" "^0.0.6"
+ use-sync-external-store "^1.4.0"
+
react-refresh@^0.14.0:
version "0.14.2"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
@@ -5768,6 +5830,23 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+recharts@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.0.2.tgz#f81f411f57d5e41a9ab9fc5817be4a58a2181046"
+ integrity sha512-eDc3ile9qJU9Dp/EekSthQPhAVPG48/uM47jk+PF7VBQngxeW3cwQpPHb/GHC1uqwyCRWXcIrDzuHRVrnRryoQ==
+ dependencies:
+ "@reduxjs/toolkit" "1.x.x || 2.x.x"
+ clsx "^2.1.1"
+ decimal.js-light "^2.5.1"
+ es-toolkit "^1.39.3"
+ eventemitter3 "^5.0.1"
+ immer "^10.1.1"
+ react-redux "8.x.x || 9.x.x"
+ reselect "5.1.1"
+ tiny-invariant "^1.3.3"
+ use-sync-external-store "^1.2.2"
+ victory-vendor "^37.0.2"
+
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -5776,6 +5855,16 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
+redux-thunk@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
+ integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
+
+redux@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
+ integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
+
regenerate-unicode-properties@^10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0"
@@ -5843,6 +5932,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+reselect@5.1.1, reselect@^5.1.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
+ integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
+
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -6331,6 +6425,11 @@ tiny-inflate@^1.0.0, tiny-inflate@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+tiny-invariant@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
+ integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
+
tinybench@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
@@ -6555,7 +6654,7 @@ use-isomorphic-layout-effect@^1.1.2:
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz#afb292eb284c39219e8cb8d3d62d71999361a21d"
integrity sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==
-use-sync-external-store@^1.2.0:
+use-sync-external-store@^1.2.0, use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
@@ -6588,6 +6687,26 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+victory-vendor@^37.0.2:
+ version "37.3.6"
+ resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-37.3.6.tgz#401ac4b029a0b3d33e0cba8e8a1d765c487254da"
+ integrity sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==
+ dependencies:
+ "@types/d3-array" "^3.0.3"
+ "@types/d3-ease" "^3.0.0"
+ "@types/d3-interpolate" "^3.0.1"
+ "@types/d3-scale" "^4.0.2"
+ "@types/d3-shape" "^3.1.0"
+ "@types/d3-time" "^3.0.0"
+ "@types/d3-timer" "^3.0.0"
+ d3-array "^3.1.6"
+ d3-ease "^3.0.1"
+ d3-interpolate "^3.0.1"
+ d3-scale "^4.0.2"
+ d3-shape "^3.1.0"
+ d3-time "^3.0.0"
+ d3-timer "^3.0.1"
+
vite-node@3.0.0-beta.2:
version "3.0.0-beta.2"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.0.0-beta.2.tgz#4208a6be384f9e7bba97570114d662ce9c957dc1"