From 381ae4b5428bd6bbef19d517850eca1eb4172c02 Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Thu, 5 Jun 2025 16:33:01 +0900 Subject: [PATCH 01/18] =?UTF-8?q?config:=20bundle=20size=20visualizer=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- stats.html | 4949 ++++++++++++++++++++++++++++++++++++++++++++++++ vite.config.ts | 11 +- yarn.lock | 43 +- 4 files changed, 5004 insertions(+), 4 deletions(-) create mode 100644 stats.html diff --git a/package.json b/package.json index 05c9eac..6432c70 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "jsdom": "^26.1.0", "lint-staged": "^15.5.1", "msw": "^2.8.2", + "rollup-plugin-visualizer": "^6.0.1", "tailwindcss": "^4.1.5", "typescript": "~5.7.2", "vite": "^6.3.1", @@ -62,6 +63,8 @@ }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "msw": { - "workerDirectory": ["public"] + "workerDirectory": [ + "public" + ] } } diff --git a/stats.html b/stats.html new file mode 100644 index 0000000..eefae3a --- /dev/null +++ b/stats.html @@ -0,0 +1,4949 @@ + + + + + + + + Rollup Visualizer + + + +
+ + + + + diff --git a/vite.config.ts b/vite.config.ts index b3aa91b..618da96 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,18 @@ import { reactRouter } from '@react-router/dev/vite'; import svgr from '@svgr/rollup'; import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; +import { visualizer } from 'rollup-plugin-visualizer'; +import { type PluginOption, defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ - plugins: [svgr(), tailwindcss(), reactRouter(), tsconfigPaths()], + plugins: [ + svgr(), + tailwindcss(), + reactRouter(), + tsconfigPaths(), + visualizer() as PluginOption, + ], optimizeDeps: { exclude: ['@amcharts/amcharts5'], }, diff --git a/yarn.lock b/yarn.lock index 2e02f88..e0633f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3108,6 +3108,11 @@ define-data-property@^1.0.1, define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -3772,6 +3777,11 @@ is-date-object@^1.0.5: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -3819,6 +3829,13 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isbot@^5: version "5.1.27" resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.27.tgz#6c4683b273d76c28b2f2ad375898f0602c9304d2" @@ -4447,6 +4464,15 @@ onetime@^7.0.0: dependencies: mimic-function "^5.0.0" +open@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + outvariant@^1.4.0, outvariant@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" @@ -4819,6 +4845,16 @@ robust-predicates@^3.0.2: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== +rollup-plugin-visualizer@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.1.tgz#dd00e7295335bb3a8a5fd8ec0ed922f479b31e00" + integrity sha512-NjlGElvLXCSZSAi3gNRZbfX3qlQbQcJ9TW97c5JpqfVwMhttj9YwEdPwcvbKj91RnMX2PWAjonvSEv6UEYtnRQ== + dependencies: + open "^8.0.0" + picomatch "^4.0.2" + source-map "^0.7.4" + yargs "^17.5.1" + rollup@^4.34.9: version "4.40.2" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.2.tgz#778e88b7a197542682b3e318581f7697f55f0619" @@ -5070,6 +5106,11 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -5723,7 +5764,7 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.7.2: +yargs@^17.5.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== From 16fc1e0d806d7b3ba53a587f2fdba39921896ea7 Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Thu, 5 Jun 2025 17:46:44 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat:=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/trade.$ticker.tsx | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/routes/trade.$ticker.tsx b/src/app/routes/trade.$ticker.tsx index 1f05905..314e2f0 100644 --- a/src/app/routes/trade.$ticker.tsx +++ b/src/app/routes/trade.$ticker.tsx @@ -49,7 +49,7 @@ export default function TradeRouteComponent({ })); return ( -
+
)} -
-
+
+
- 실시간 체결 목록 - {coinInfo && } + 실시간 차트 + {coinInfo && ( + + )}
-
+
주문 하기 {isLoggedIn && coinInfo ? ( @@ -77,24 +79,22 @@ export default function TradeRouteComponent({ )}
-
+
실시간 호가 {coinInfo && }
-
+
- 가상화폐 리스트 - + 실시간 체결 목록 + {coinInfo && }
-
+
- 실시간 차트 - {coinInfo && ( - - )} + 가상화폐 리스트 +
From 789a8a2f301116c13a86327dd55e4a48974cfa23 Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Thu, 5 Jun 2025 17:46:44 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/trade.$ticker.tsx | 53 +++++++++++++++-------- src/assets/svgs/bars-solid.svg | 1 + src/assets/svgs/index.ts | 2 + src/assets/svgs/xmark-solid.svg | 1 + src/features/chat/ui/ChatButton/index.tsx | 2 +- src/shared/ui/CloseButton/index.tsx | 17 ++++++++ src/shared/ui/MenuButton/index.tsx | 17 ++++++++ src/widgets/navbar/index.ts | 1 + src/widgets/navbar/ui/NavBar/index.tsx | 9 +++- src/widgets/navbar/ui/SideBar/index.tsx | 32 ++++++++++++++ 10 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 src/assets/svgs/bars-solid.svg create mode 100644 src/assets/svgs/xmark-solid.svg create mode 100644 src/shared/ui/CloseButton/index.tsx create mode 100644 src/shared/ui/MenuButton/index.tsx create mode 100644 src/widgets/navbar/ui/SideBar/index.tsx diff --git a/src/app/routes/trade.$ticker.tsx b/src/app/routes/trade.$ticker.tsx index 1f05905..c368b72 100644 --- a/src/app/routes/trade.$ticker.tsx +++ b/src/app/routes/trade.$ticker.tsx @@ -1,4 +1,5 @@ import * as cookie from 'cookie'; +import { useState } from 'react'; import { Outlet, redirect } from 'react-router'; import { CoinPriceWithName, api as coinApi } from '~/entities/coin'; @@ -10,7 +11,7 @@ import { ExecutionList } from '~/features/order-execution-list'; import { Orderbook, StockChart } from '~/features/tradeview'; import Container from '~/shared/ui/Container'; import ContainerTitle from '~/shared/ui/ContainerTitle'; -import { NavBar } from '~/widgets/navbar'; +import { NavBar, SideBar } from '~/widgets/navbar'; import type { Route } from './+types/trade.$ticker'; export async function loader({ request, params }: Route.LoaderArgs) { @@ -39,35 +40,45 @@ export async function clientAction() { export default function TradeRouteComponent({ loaderData, }: Route.ComponentProps) { - const coinInfo = loaderData.coinInfo; - const isLoggedIn = loaderData.isLoggedIn; - const coinList = loaderData.coinList; + const [isMenuOpen, setIsMenuOpen] = useState(false); + const { coinInfo, coinList, isLoggedIn } = loaderData; const coinListWithIcon = coinList.map((coinInfo) => ({ ...coinInfo, coinIcon: 🪙, to: `/trade/${coinInfo.ticker}`, })); + const handleOpenMenu = () => { + setIsMenuOpen(true); + }; + + const handleCloseMenu = () => { + setIsMenuOpen(false); + }; + return ( -
+
{coinInfo && ( )} -
-
+
+
- 실시간 체결 목록 - {coinInfo && } + 실시간 차트 + {coinInfo && ( + + )}
-
+
주문 하기 {isLoggedIn && coinInfo ? ( @@ -77,27 +88,31 @@ export default function TradeRouteComponent({ )}
-
+
실시간 호가 {coinInfo && }
-
+
- 가상화폐 리스트 - + 실시간 체결 목록 + {coinInfo && }
-
+
- 실시간 차트 - {coinInfo && ( - - )} + 가상화폐 리스트 +
+ {isMenuOpen && ( + + )}
diff --git a/src/assets/svgs/bars-solid.svg b/src/assets/svgs/bars-solid.svg new file mode 100644 index 0000000..98f5de2 --- /dev/null +++ b/src/assets/svgs/bars-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svgs/index.ts b/src/assets/svgs/index.ts index 0848700..1cb6b38 100644 --- a/src/assets/svgs/index.ts +++ b/src/assets/svgs/index.ts @@ -4,3 +4,5 @@ export { ReactComponent as IconMagnifying } from './magnifying.svg'; export { ReactComponent as IconPlus } from './plus-solid.svg'; export { ReactComponent as IconMinus } from './minus-solid.svg'; export { ReactComponent as IconHeadset } from './headset-solid.svg'; +export { ReactComponent as IconBars } from './bars-solid.svg'; +export { ReactComponent as IconXmark } from './xmark-solid.svg'; diff --git a/src/assets/svgs/xmark-solid.svg b/src/assets/svgs/xmark-solid.svg new file mode 100644 index 0000000..db6aca6 --- /dev/null +++ b/src/assets/svgs/xmark-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/features/chat/ui/ChatButton/index.tsx b/src/features/chat/ui/ChatButton/index.tsx index 9f29b6a..cfc24c3 100644 --- a/src/features/chat/ui/ChatButton/index.tsx +++ b/src/features/chat/ui/ChatButton/index.tsx @@ -22,7 +22,7 @@ export default function ChatButton({ isOpen, handleClick }: ChatButtonProps) { void; +}; + +export default function CloseButton({ onClick }: CloseButtonProps) { + return ( + + ); +} diff --git a/src/shared/ui/MenuButton/index.tsx b/src/shared/ui/MenuButton/index.tsx new file mode 100644 index 0000000..031b30a --- /dev/null +++ b/src/shared/ui/MenuButton/index.tsx @@ -0,0 +1,17 @@ +import { IconBars } from '~/assets/svgs'; + +export type MenuButtonProps = { + onClick: () => void; +}; + +export default function MenuButton({ onClick }: MenuButtonProps) { + return ( + + ); +} diff --git a/src/widgets/navbar/index.ts b/src/widgets/navbar/index.ts index 7d9eb33..6022be7 100644 --- a/src/widgets/navbar/index.ts +++ b/src/widgets/navbar/index.ts @@ -1 +1,2 @@ export { default as NavBar } from './ui/NavBar'; +export { default as SideBar } from './ui/SideBar'; diff --git a/src/widgets/navbar/ui/NavBar/index.tsx b/src/widgets/navbar/ui/NavBar/index.tsx index be0eeaf..2c0df3d 100644 --- a/src/widgets/navbar/ui/NavBar/index.tsx +++ b/src/widgets/navbar/ui/NavBar/index.tsx @@ -1,14 +1,17 @@ import { Link, type LinkProps, NavLink, useSubmit } from 'react-router'; + import type { CoinTicker } from '~/entities/coin'; import Button from '~/shared/ui/Button'; import LogoWithTitle, { type LogoWithTitleProps, } from '~/shared/ui/LogoWithTitle'; +import MenuButton from '~/shared/ui/MenuButton'; export type NavBarProps = { to: LinkProps['to']; isLoggedIn?: boolean; ticker?: CoinTicker; + onClickMenuButton: () => void; } & LogoWithTitleProps; export default function NavBar({ @@ -17,6 +20,7 @@ export default function NavBar({ isBlack, isLoggedIn, ticker, + onClickMenuButton, }: NavBarProps) { const submit = useSubmit(); @@ -34,7 +38,10 @@ export default function NavBar({ return ( <> -
- {size} + {size.toFixed(6)}
{changeRate.toFixed(2)}% From 2249668f992453b73f060c161bcca4dbc92b0ead Mon Sep 17 00:00:00 2001 From: caniro Date: Sat, 7 Jun 2025 22:04:57 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=EC=B2=B4=EA=B2=B0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/root.tsx | 7 +++ .../trade/hooks/useTradeNotification.tsx | 52 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/features/trade/hooks/useTradeNotification.tsx diff --git a/src/app/root.tsx b/src/app/root.tsx index 19755ac..ab64b0d 100644 --- a/src/app/root.tsx +++ b/src/app/root.tsx @@ -12,6 +12,7 @@ import type { Route } from './+types/root'; import './app.css'; import { Slide } from 'react-toastify'; +import useTradeNotification from '~/features/trade/hooks/useTradeNotification'; import StompProvider from './provider/StompProvider'; export const links: Route.LinksFunction = () => [ @@ -74,9 +75,15 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } +function TradeNotificationHandler() { + useTradeNotification(); + return null; +} + export default function App() { return ( + (null); + + useEffect(() => { + const fetchUserInfo = async () => { + try { + const response = await userApi.getUserInfo(); + const { data } = await (response.json() as Promise); + setUserId(data.userId); + } catch (error) { + console.error('Failed to fetch user info:', error); + toast.error('사용자 정보를 가져오는데 실패했습니다.'); + } + }; + + fetchUserInfo(); + }, []); + + useEffect(() => { + if (!client || !connected || !userId) return; + + const subscription = client.subscribe( + `/topic/tradeNotification/${userId}`, + (message) => { + const parsedData = JSON.parse(message.body) as TradeNotification; + const tradeType = parsedData.type === 'ask' ? '매도' : '매수'; + const toastMessage = `${parsedData.ticker} ${tradeType} 체결 완료 - 가격: ${parsedData.price}, 수량: ${parsedData.size}`; + toast.success(toastMessage); + }, + ); + + return () => { + subscription.unsubscribe(); + }; + }, [client, connected, userId]); +} From 0fa40717e64104228759147eae377ad827a604b4 Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sat, 7 Jun 2025 22:49:03 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B0=80=EA=B2=A9=20=ED=8F=AC=EB=A9=A7=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/coin-search-list/ui/CoinListItem/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/coin-search-list/ui/CoinListItem/index.tsx b/src/features/coin-search-list/ui/CoinListItem/index.tsx index b039b03..c595fc5 100644 --- a/src/features/coin-search-list/ui/CoinListItem/index.tsx +++ b/src/features/coin-search-list/ui/CoinListItem/index.tsx @@ -5,6 +5,7 @@ import { type CoinWithIconAndNameProps, useCurrentPrice, } from '~/entities/coin'; +import { formatCurrencyKR } from '~/shared/utils'; export type CoinListItemProps = { to: LinkProps['to']; @@ -18,6 +19,7 @@ export default function CoinListItem({ }: CoinListItemProps) { const currentPriceData = useCurrentPrice(ticker); const isBull = currentPriceData && currentPriceData.changeRate > 0; + const formatedPrice = `${formatCurrencyKR(+(currentPriceData?.currentPrice || 0).toFixed(2))}원`; return ( @@ -34,7 +36,7 @@ export default function CoinListItem({
- {currentPriceData?.currentPrice} + {formatedPrice}
From 6ff0bb4f5910680c02328fa847be18cae203935b Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 00:30:50 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/trade.$ticker.tsx | 116 ++++++++++++------------ src/shared/ui/Backdrop/index.tsx | 19 +++- src/widgets/navbar/ui/SideBar/index.tsx | 20 +++- 3 files changed, 93 insertions(+), 62 deletions(-) diff --git a/src/app/routes/trade.$ticker.tsx b/src/app/routes/trade.$ticker.tsx index c368b72..a2d9db7 100644 --- a/src/app/routes/trade.$ticker.tsx +++ b/src/app/routes/trade.$ticker.tsx @@ -1,4 +1,5 @@ import * as cookie from 'cookie'; +import { AnimatePresence } from 'motion/react'; import { useState } from 'react'; import { Outlet, redirect } from 'react-router'; @@ -58,63 +59,66 @@ export default function TradeRouteComponent({ return (
- - {coinInfo && ( - - )} -
-
- - 실시간 차트 - {coinInfo && ( - - )} - -
-
- - 주문 하기 - {isLoggedIn && coinInfo ? ( - - ) : ( - - )} - -
-
- - 실시간 호가 - {coinInfo && } - -
-
- - 실시간 체결 목록 - {coinInfo && } - -
-
- - 가상화폐 리스트 - - -
-
- {isMenuOpen && ( - + - )} - - + {coinInfo && ( + + )} +
+
+ + 실시간 차트 + {coinInfo && ( + + )} + +
+
+ + 주문 하기 + {isLoggedIn && coinInfo ? ( + + ) : ( + + )} + +
+
+ + 실시간 호가 + {coinInfo && } + +
+
+ + 실시간 체결 목록 + {coinInfo && } + +
+
+ + 가상화폐 리스트 + + +
+
+ {isMenuOpen && ( + + )} + + +
); } diff --git a/src/shared/ui/Backdrop/index.tsx b/src/shared/ui/Backdrop/index.tsx index 06e6324..50bb796 100644 --- a/src/shared/ui/Backdrop/index.tsx +++ b/src/shared/ui/Backdrop/index.tsx @@ -1,14 +1,25 @@ +import { motion } from 'motion/react'; import type { HTMLAttributes } from 'react'; export type BackdropProps = HTMLAttributes; -export default function Backdrop({ children, ...props }: BackdropProps) { +const backdropVariant = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 }, +}; + +export default function Backdrop({ children }: BackdropProps) { return ( -
{children} -
+ ); } diff --git a/src/widgets/navbar/ui/SideBar/index.tsx b/src/widgets/navbar/ui/SideBar/index.tsx index ea63f2b..71cd6a3 100644 --- a/src/widgets/navbar/ui/SideBar/index.tsx +++ b/src/widgets/navbar/ui/SideBar/index.tsx @@ -1,3 +1,4 @@ +import { motion } from 'motion/react'; import { useRef } from 'react'; import { @@ -14,19 +15,34 @@ type SideBarProps = { onClose: () => void; }; +const sideBarVariant = { + initial: { x: '-100%' }, + animate: { x: 0 }, + exit: { x: '-100%' }, +}; + export default function SideBar({ coinListWithIcon, onClose }: SideBarProps) { const ref = useRef(null); useClickOutside(ref, onClose); return ( -
+
가상화폐 리스트
-
+
); } From 0a503f56f26c8c0919df0d803bbce717737ee01b Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 00:33:17 +0900 Subject: [PATCH 08/18] =?UTF-8?q?fix:=20ticker=EA=B0=80=20=EB=B0=94?= =?UTF-8?q?=EB=80=8C=EB=A9=B4=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=B1=84?= =?UTF-8?q?=EA=B2=B0=EB=AA=A9=EB=A1=9D=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/trade.$ticker.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/routes/trade.$ticker.tsx b/src/app/routes/trade.$ticker.tsx index a2d9db7..5541525 100644 --- a/src/app/routes/trade.$ticker.tsx +++ b/src/app/routes/trade.$ticker.tsx @@ -99,7 +99,9 @@ export default function TradeRouteComponent({
실시간 체결 목록 - {coinInfo && } + {coinInfo && ( + + )}
From 4b509a468f8e191ac5105ec94d65000c47c27cff Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 01:07:40 +0900 Subject: [PATCH 09/18] =?UTF-8?q?fix:=20AIChatBot=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/chat/ui/AIChatBot/index.tsx | 3 ++- src/features/chat/ui/ChatButton/index.tsx | 2 +- src/features/chat/ui/ChatWindow/index.tsx | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/chat/ui/AIChatBot/index.tsx b/src/features/chat/ui/AIChatBot/index.tsx index aee31ca..b2d6c95 100644 --- a/src/features/chat/ui/AIChatBot/index.tsx +++ b/src/features/chat/ui/AIChatBot/index.tsx @@ -31,7 +31,7 @@ export default function AIChatBot() { }; return ( - + {isOpen ? ( {state.context.messageList.map((message, index) => { const key = `msg-${index}-${message.isMine ? 'user' : 'ai'}`; diff --git a/src/features/chat/ui/ChatButton/index.tsx b/src/features/chat/ui/ChatButton/index.tsx index cfc24c3..1cf9b88 100644 --- a/src/features/chat/ui/ChatButton/index.tsx +++ b/src/features/chat/ui/ChatButton/index.tsx @@ -22,7 +22,7 @@ export default function ChatButton({ isOpen, handleClick }: ChatButtonProps) { Date: Sun, 8 Jun 2025 01:42:37 +0900 Subject: [PATCH 10/18] =?UTF-8?q?fix:=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/trade.$ticker.tsx | 12 ++++++------ src/features/tradeview/ui/StockChart/index.tsx | 2 +- src/shared/ui/Container/index.tsx | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/routes/trade.$ticker.tsx b/src/app/routes/trade.$ticker.tsx index 5541525..809157a 100644 --- a/src/app/routes/trade.$ticker.tsx +++ b/src/app/routes/trade.$ticker.tsx @@ -58,7 +58,7 @@ export default function TradeRouteComponent({ }; return ( -
+
)} -
-
+
+
실시간 차트 {coinInfo && ( @@ -80,7 +80,7 @@ export default function TradeRouteComponent({ )}
-
+
주문 하기 {isLoggedIn && coinInfo ? ( @@ -90,13 +90,13 @@ export default function TradeRouteComponent({ )}
-
+
실시간 호가 {coinInfo && }
-
+
실시간 체결 목록 {coinInfo && ( diff --git a/src/features/tradeview/ui/StockChart/index.tsx b/src/features/tradeview/ui/StockChart/index.tsx index 5bfa575..27cdec2 100644 --- a/src/features/tradeview/ui/StockChart/index.tsx +++ b/src/features/tradeview/ui/StockChart/index.tsx @@ -503,7 +503,7 @@ export default function StockChart({ ticker }: StockChartProps) { }, []); return ( -
+
diff --git a/src/shared/ui/Container/index.tsx b/src/shared/ui/Container/index.tsx index a3b54ff..9e78b27 100644 --- a/src/shared/ui/Container/index.tsx +++ b/src/shared/ui/Container/index.tsx @@ -6,7 +6,7 @@ type ContainerProps = { export default function Container({ children }: ContainerProps) { return ( -
+
{children}
); From ac773422d6632de69ccab3c059c71a20f216ca0a Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 02:13:37 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20framer=20motion=20duplicate=20key?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AnimatePresence 컴포넌트의 바로 아래 자식은 유일한 key를 가지고 있지 않으면 deplicate key 에러가 발생합니다. --- src/app/routes/trade.$ticker.login.tsx | 2 +- src/app/routes/trade.$ticker.tsx | 109 ++++++++++++------------ src/shared/ui/Backdrop/index.tsx | 1 - src/widgets/navbar/ui/SideBar/index.tsx | 1 - 4 files changed, 57 insertions(+), 56 deletions(-) diff --git a/src/app/routes/trade.$ticker.login.tsx b/src/app/routes/trade.$ticker.login.tsx index 23e0042..6b67006 100644 --- a/src/app/routes/trade.$ticker.login.tsx +++ b/src/app/routes/trade.$ticker.login.tsx @@ -1,5 +1,5 @@ import { LoginModal } from '~/widgets/auth'; export default function LoginRouteComponent() { - return ; + return ; } diff --git a/src/app/routes/trade.$ticker.tsx b/src/app/routes/trade.$ticker.tsx index 809157a..693438d 100644 --- a/src/app/routes/trade.$ticker.tsx +++ b/src/app/routes/trade.$ticker.tsx @@ -59,67 +59,70 @@ export default function TradeRouteComponent({ return (
- - - {coinInfo && ( - - )} -
-
- - 실시간 차트 - {coinInfo && ( - - )} - -
-
- - 주문 하기 - {isLoggedIn && coinInfo ? ( - - ) : ( - - )} - -
-
- - 실시간 호가 - {coinInfo && } - -
-
- - 실시간 체결 목록 - {coinInfo && ( - - )} - -
-
- - 가상화폐 리스트 - - -
+ + {coinInfo && ( + + )} +
+
+ + 실시간 차트 + {coinInfo && ( + + )} + +
+
+ + 주문 하기 + {isLoggedIn && coinInfo ? ( + + ) : ( + + )} + +
+
+ + 실시간 호가 + {coinInfo && } +
+
+ + 실시간 체결 목록 + {coinInfo && ( + + )} + +
+
+ + 가상화폐 리스트 + + +
+
+ {isMenuOpen && ( )} - +
); diff --git a/src/shared/ui/Backdrop/index.tsx b/src/shared/ui/Backdrop/index.tsx index 50bb796..bce2e6e 100644 --- a/src/shared/ui/Backdrop/index.tsx +++ b/src/shared/ui/Backdrop/index.tsx @@ -17,7 +17,6 @@ export default function Backdrop({ children }: BackdropProps) { initial="initial" animate="animate" exit="exit" - key="backdrop" > {children} diff --git a/src/widgets/navbar/ui/SideBar/index.tsx b/src/widgets/navbar/ui/SideBar/index.tsx index 71cd6a3..4ca4552 100644 --- a/src/widgets/navbar/ui/SideBar/index.tsx +++ b/src/widgets/navbar/ui/SideBar/index.tsx @@ -35,7 +35,6 @@ export default function SideBar({ coinListWithIcon, onClose }: SideBarProps) { animate="animate" exit="exit" transition={{ ease: 'easeIn' }} - key="sidebar" >
가상화폐 리스트 From 14278c64aa0eeca7a5715e18ebcabcd3c3de5b3b Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 11:51:24 +0900 Subject: [PATCH 12/18] =?UTF-8?q?test:=20=EC=8B=A4=ED=8C=A8=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/chat/ui/AIChatBot/AIChatBot.test.tsx | 14 ++++++++++---- .../ui/ExecutionItem/ExecutionItem.test.tsx | 10 ++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/features/chat/ui/AIChatBot/AIChatBot.test.tsx b/src/features/chat/ui/AIChatBot/AIChatBot.test.tsx index 87c6b78..7426543 100644 --- a/src/features/chat/ui/AIChatBot/AIChatBot.test.tsx +++ b/src/features/chat/ui/AIChatBot/AIChatBot.test.tsx @@ -1,4 +1,8 @@ -import { render, screen } from '@testing-library/react'; +import { + render, + screen, + waitForElementToBeRemoved, +} from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import userEvent from '@testing-library/user-event'; @@ -27,7 +31,7 @@ describe('AIChatBot 컴포넌트 테스트', () => { await user.click(chatButton); - const chatWindow = screen.getByTestId('chat-window'); + const chatWindow = await screen.findByTestId('chat-window'); expect(chatWindow).toBeInTheDocument(); }); @@ -42,13 +46,15 @@ describe('AIChatBot 컴포넌트 테스트', () => { await user.click(chatButton); - const chatWindow = screen.getByTestId('chat-window'); - const closeButton = screen.getByTestId('chat-window-close-button'); + const chatWindow = await screen.findByTestId('chat-window'); + const closeButton = await screen.findByTestId('chat-window-close-button'); expect(chatWindow).toBeInTheDocument(); await user.click(closeButton); + await waitForElementToBeRemoved(() => screen.queryByTestId('chat-window')); + expect(chatWindow).not.toBeInTheDocument(); }); }); diff --git a/src/features/order-execution-list/ui/ExecutionItem/ExecutionItem.test.tsx b/src/features/order-execution-list/ui/ExecutionItem/ExecutionItem.test.tsx index a87beac..ece15b3 100644 --- a/src/features/order-execution-list/ui/ExecutionItem/ExecutionItem.test.tsx +++ b/src/features/order-execution-list/ui/ExecutionItem/ExecutionItem.test.tsx @@ -18,8 +18,14 @@ describe('ExecutionItem 컴포넌트 테스트', () => { render(); const price = screen.getByText('1,000원'); - const size = screen.getByText('1'); - const timestamp = screen.getByText('1'); + const size = screen.getByText(String(props.size.toFixed(6))); + const timestamp = screen.getByText( + Intl.DateTimeFormat('ko-KR', { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }).format(new Date(props.timestamp)), + ); const changeRate = screen.getByText('3.00%'); expect(price).toBeInTheDocument(); From ff41c4ac4a76023c5d569f805ec2220d795dfa7f Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 12:13:41 +0900 Subject: [PATCH 13/18] =?UTF-8?q?test:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Backdrop/index.tsx | 1 + src/shared/ui/CloseButton/index.tsx | 1 + .../navbar/ui/SideBar/SideBar.test.tsx | 62 +++++++++++++++++++ src/widgets/navbar/ui/SideBar/index.tsx | 3 +- 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/widgets/navbar/ui/SideBar/SideBar.test.tsx diff --git a/src/shared/ui/Backdrop/index.tsx b/src/shared/ui/Backdrop/index.tsx index bce2e6e..68b3a05 100644 --- a/src/shared/ui/Backdrop/index.tsx +++ b/src/shared/ui/Backdrop/index.tsx @@ -17,6 +17,7 @@ export default function Backdrop({ children }: BackdropProps) { initial="initial" animate="animate" exit="exit" + data-testid="backdrop" > {children} diff --git a/src/shared/ui/CloseButton/index.tsx b/src/shared/ui/CloseButton/index.tsx index b6995c9..a31441a 100644 --- a/src/shared/ui/CloseButton/index.tsx +++ b/src/shared/ui/CloseButton/index.tsx @@ -10,6 +10,7 @@ export default function CloseButton({ onClick }: CloseButtonProps) { type="button" onClick={onClick} className="aspect-auto w-4 cursor-pointer" + data-testid="close-button" > diff --git a/src/widgets/navbar/ui/SideBar/SideBar.test.tsx b/src/widgets/navbar/ui/SideBar/SideBar.test.tsx new file mode 100644 index 0000000..ebe2fe2 --- /dev/null +++ b/src/widgets/navbar/ui/SideBar/SideBar.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createRoutesStub } from 'react-router'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import StompProvider from '~/app/provider/StompProvider'; +import SideBar from '.'; +import type { SideBarProps } from '.'; + +const props: SideBarProps = { + onClose: vi.fn(), + coinListWithIcon: [ + { coinIcon: <>, name: '비트코인', ticker: 'BTC', to: '/' }, + ], +}; + +const Stub = createRoutesStub([ + { + path: '/coin/:ticker', + Component: () => ( + + + + ), + }, +]); + +describe('SideBar 컴포넌트 테스트', () => { + beforeEach(() => { + vi.clearAllMocks(); + props.onClose = vi.fn(); + }); + + it('초기 상태에서 SideBar가 보여진다.', () => { + render(); + + const sideBar = screen.getByTestId('side-bar'); + + expect(sideBar).toBeInTheDocument(); + }); + + it('닫기 버튼을 클릭하면 onClose가 호출된다.', async () => { + const user = userEvent.setup(); + render(); + + const closeButton = screen.getByTestId('close-button'); + + await user.click(closeButton); + + expect(props.onClose).toHaveBeenCalledTimes(1); + }); + + it('Backdrop을 누르면 onClose가 호출된다.', async () => { + const user = userEvent.setup(); + render(); + + const backdrop = screen.getByTestId('backdrop'); + + await user.click(backdrop); + + expect(props.onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/widgets/navbar/ui/SideBar/index.tsx b/src/widgets/navbar/ui/SideBar/index.tsx index 4ca4552..5649eb5 100644 --- a/src/widgets/navbar/ui/SideBar/index.tsx +++ b/src/widgets/navbar/ui/SideBar/index.tsx @@ -10,7 +10,7 @@ import Backdrop from '~/shared/ui/Backdrop'; import CloseButton from '~/shared/ui/CloseButton'; import ContainerTitle from '~/shared/ui/ContainerTitle'; -type SideBarProps = { +export type SideBarProps = { coinListWithIcon: CoinListItemProps[]; onClose: () => void; }; @@ -35,6 +35,7 @@ export default function SideBar({ coinListWithIcon, onClose }: SideBarProps) { animate="animate" exit="exit" transition={{ ease: 'easeIn' }} + data-testid="side-bar" >
가상화폐 리스트 From 8db1074090d7006bcf5447c0946f83b4eb22b60b Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 12:32:05 +0900 Subject: [PATCH 14/18] =?UTF-8?q?test:=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EB=B0=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/MenuButton/index.tsx | 1 + src/widgets/navbar/index.ts | 2 + src/widgets/navbar/ui/NavBar/NavBar.test.tsx | 81 ++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/widgets/navbar/ui/NavBar/NavBar.test.tsx diff --git a/src/shared/ui/MenuButton/index.tsx b/src/shared/ui/MenuButton/index.tsx index 031b30a..5545a33 100644 --- a/src/shared/ui/MenuButton/index.tsx +++ b/src/shared/ui/MenuButton/index.tsx @@ -10,6 +10,7 @@ export default function MenuButton({ onClick }: MenuButtonProps) { type="button" onClick={onClick} className="aspect-auto w-4 cursor-pointer" + data-testid="menu-button" > diff --git a/src/widgets/navbar/index.ts b/src/widgets/navbar/index.ts index 6022be7..e454122 100644 --- a/src/widgets/navbar/index.ts +++ b/src/widgets/navbar/index.ts @@ -1,2 +1,4 @@ +/* v8 ignore start */ export { default as NavBar } from './ui/NavBar'; export { default as SideBar } from './ui/SideBar'; +/* v8 ignore end */ diff --git a/src/widgets/navbar/ui/NavBar/NavBar.test.tsx b/src/widgets/navbar/ui/NavBar/NavBar.test.tsx new file mode 100644 index 0000000..14fe0f5 --- /dev/null +++ b/src/widgets/navbar/ui/NavBar/NavBar.test.tsx @@ -0,0 +1,81 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import type { ReactNode } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import type { NavBarProps } from '.'; +import NavBar from '.'; + +const props: NavBarProps = { + to: '/', + serviceName: 'IF', + ticker: 'BTC', + isLoggedIn: true, + onClickMenuButton: vi.fn(), +}; + +const mockSubmit = vi.fn(); + +vi.mock('react-router', () => ({ + useSubmit: () => mockSubmit, + Link: ({ children, to }: { children: ReactNode; to: string }) => ( + {children} + ), + NavLink: ({ children, to }: { children: ReactNode; to: string }) => ( + {children} + ), +})); + +describe('NavBar 컴포넌트 테스트', () => { + it('초기 상태에서 NavBar가 보여진다.', () => { + render(); + + const navBar = screen.getByRole('navigation'); + + expect(navBar).toBeInTheDocument(); + }); + + it('로그인이 되어있으면 로그아웃 버튼이 보여진다.', () => { + render(); + + const logoutButton = screen.getByRole('button', { name: '로그아웃' }); + + expect(logoutButton).toBeInTheDocument(); + }); + + it('로그아웃이 되어있으면 로그인 버튼이 보인다.', () => { + render(); + + const loginButton = screen.getByRole('button', { name: '로그인' }); + + expect(loginButton).toBeInTheDocument(); + }); + + it('로그아웃 버튼을 누르면 submit으로 액션을 발생시킨다.', async () => { + const user = userEvent.setup(); + render(); + + const logoutButton = screen.getByRole('button', { name: '로그아웃' }); + + await user.click(logoutButton); + + expect(mockSubmit).toHaveBeenNthCalledWith( + 1, + null, + expect.objectContaining({ + action: '/trade/BTC', + method: 'post', + }), + ); + }); + + it('메뉴 버튼을 누르면 onClickMenuButton이 호출된다.', async () => { + const user = userEvent.setup(); + render(); + + const menuButton = screen.getByTestId('menu-button'); + + await user.click(menuButton); + + expect(props.onClickMenuButton).toHaveBeenCalledTimes(1); + }); +}); From 55eaad0cd3b875b851a4d5342e84c197f4af1440 Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 12:39:44 +0900 Subject: [PATCH 15/18] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/auth/index.ts | 2 ++ .../auth/ui/LoginModal/LoginModal.test.tsx | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/widgets/auth/ui/LoginModal/LoginModal.test.tsx diff --git a/src/widgets/auth/index.ts b/src/widgets/auth/index.ts index f1919cc..1482ab3 100644 --- a/src/widgets/auth/index.ts +++ b/src/widgets/auth/index.ts @@ -1 +1,3 @@ +/* v8 ignore start */ export { default as LoginModal } from './ui/LoginModal'; +/* v8 ignore end */ diff --git a/src/widgets/auth/ui/LoginModal/LoginModal.test.tsx b/src/widgets/auth/ui/LoginModal/LoginModal.test.tsx new file mode 100644 index 0000000..992e311 --- /dev/null +++ b/src/widgets/auth/ui/LoginModal/LoginModal.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import userEvent from '@testing-library/user-event'; +import LoginModal from '.'; + +const navigate = vi.fn(); + +vi.mock('react-router', () => ({ + useNavigate: () => navigate, +})); + +describe('LoginModal 컴포넌트 테스트', () => { + it('LoginModal이 렌더링된다.', () => { + render(); + + const loginModal = screen.getByRole('dialog'); + + expect(loginModal).toBeInTheDocument(); + }); + + it('Backdrop을 클릭하면 navigate가 호출된다.', async () => { + const user = userEvent.setup(); + render(); + + const backdrop = screen.getByTestId('backdrop'); + + await user.click(backdrop); + + expect(navigate).toHaveBeenNthCalledWith(1, -1); + }); +}); From 05974aee998100e9e9bd01ff75e475a65b918b6e Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 21:06:24 +0900 Subject: [PATCH 16/18] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=9B=84=20userId=EB=A5=BC=20=EC=A0=80=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EB=8A=94=20Provider=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/provider/UserInfoProvider.tsx | 60 ++++++++++++++++++++++++++ src/app/root.tsx | 39 +++++++++-------- src/app/routes/callback.tsx | 35 ++++++++++----- src/widgets/navbar/ui/NavBar/index.tsx | 3 ++ 4 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 src/app/provider/UserInfoProvider.tsx diff --git a/src/app/provider/UserInfoProvider.tsx b/src/app/provider/UserInfoProvider.tsx new file mode 100644 index 0000000..105f07c --- /dev/null +++ b/src/app/provider/UserInfoProvider.tsx @@ -0,0 +1,60 @@ +import { + type ReactNode, + createContext, + useContext, + useEffect, + useState, +} from 'react'; +import type { UserInfoResponse } from '~/entities/user/types/user.type'; + +type UserInfoContextType = { + userId: UserInfoResponse['data']['userId'] | null; + setUserId: (userId: UserInfoResponse['data']['userId'] | null) => void; +}; + +type UserIdProviderProps = { + children: ReactNode; +}; + +export const UserIdContext = createContext(null); + +export default function UserIdProvider({ children }: UserIdProviderProps) { + const [userId, setUserId] = useState( + null, + ); + + useEffect(() => { + const storedUserId = window.localStorage.getItem('userId'); + + if (!storedUserId) return; + + setUserId(Number(storedUserId)); + }, []); + + useEffect(() => { + if (!userId) { + window.localStorage.removeItem('userId'); + return; + } + + window.localStorage.setItem('userId', String(userId)); + }, [userId]); + + return ( + + {children} + + ); +} + +export function useUserId() { + const context = useContext(UserIdContext); + + if (!context) { + throw new Error( + 'useUserId hook은 UserIdProvider 내부에서 사용해야 합니다.', + ); + } + + return context; +} diff --git a/src/app/root.tsx b/src/app/root.tsx index 19755ac..59e6163 100644 --- a/src/app/root.tsx +++ b/src/app/root.tsx @@ -13,6 +13,7 @@ import type { Route } from './+types/root'; import './app.css'; import { Slide } from 'react-toastify'; import StompProvider from './provider/StompProvider'; +import UserIdProvider from './provider/UserInfoProvider'; export const links: Route.LinksFunction = () => [ { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, @@ -76,23 +77,27 @@ export function Layout({ children }: { children: React.ReactNode }) { export default function App() { return ( - - - - + + + + + + ); } diff --git a/src/app/routes/callback.tsx b/src/app/routes/callback.tsx index 7df2d3e..f55355b 100644 --- a/src/app/routes/callback.tsx +++ b/src/app/routes/callback.tsx @@ -1,5 +1,11 @@ import * as cookie from 'cookie'; -import { type LoaderFunctionArgs, redirect } from 'react-router'; +import { type LoaderFunctionArgs, redirect, useNavigate } from 'react-router'; +import type { Route } from './+types/callback'; + +import { useEffect } from 'react'; +import type { UserInfoResponse } from '~/entities/user/types/user.type'; +import ApiClient from '~/shared/api/httpClient'; +import { useUserId } from '../provider/UserInfoProvider'; export async function loader({ request }: LoaderFunctionArgs) { const rawCookie = request.headers.get('Cookie'); @@ -10,17 +16,26 @@ export async function loader({ request }: LoaderFunctionArgs) { return redirect('/trade/BTC/login'); } - return redirect('/trade'); + const response = await ApiClient.get('api/userinfo', { + headers: { + Cookie: rawCookie || '', + }, + }); + + const { data } = await response.json(); - // 이전로직: - // try { - // await api.checkToken(); - // } catch (error) { - // return redirect('/trade/login'); - // } - // return redirect('/trade'); + return data.userId; } -export default function CallbackRoutes() { +export default function CallbackRoutes({ loaderData }: Route.ComponentProps) { + const navigate = useNavigate(); + const { userId, setUserId } = useUserId(); + setUserId(loaderData); + + useEffect(() => { + if (!userId) return; + navigate('/trade/BTC'); + }, [userId, navigate]); + return null; } diff --git a/src/widgets/navbar/ui/NavBar/index.tsx b/src/widgets/navbar/ui/NavBar/index.tsx index 2c0df3d..9459ea7 100644 --- a/src/widgets/navbar/ui/NavBar/index.tsx +++ b/src/widgets/navbar/ui/NavBar/index.tsx @@ -1,4 +1,5 @@ import { Link, type LinkProps, NavLink, useSubmit } from 'react-router'; +import { useUserId } from '~/app/provider/UserInfoProvider'; import type { CoinTicker } from '~/entities/coin'; import Button from '~/shared/ui/Button'; @@ -23,8 +24,10 @@ export default function NavBar({ onClickMenuButton, }: NavBarProps) { const submit = useSubmit(); + const { setUserId } = useUserId(); const handleLogout = () => { + setUserId(null); submit(null, { action: `/trade/${ticker}`, method: 'post' }); }; From 2b67f5f46aa8b8391bec5d7f2434f3ca80b1aa7f Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 21:09:01 +0900 Subject: [PATCH 17/18] =?UTF-8?q?test:=20=EC=8B=A4=ED=8C=A8=ED=95=9C=20Nav?= =?UTF-8?q?Bar=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/navbar/ui/NavBar/NavBar.test.tsx | 31 ++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/widgets/navbar/ui/NavBar/NavBar.test.tsx b/src/widgets/navbar/ui/NavBar/NavBar.test.tsx index 14fe0f5..eb2fef0 100644 --- a/src/widgets/navbar/ui/NavBar/NavBar.test.tsx +++ b/src/widgets/navbar/ui/NavBar/NavBar.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; +import UserIdProvider from '~/app/provider/UserInfoProvider'; import type { NavBarProps } from '.'; import NavBar from '.'; @@ -27,7 +28,11 @@ vi.mock('react-router', () => ({ describe('NavBar 컴포넌트 테스트', () => { it('초기 상태에서 NavBar가 보여진다.', () => { - render(); + render(, { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); const navBar = screen.getByRole('navigation'); @@ -35,7 +40,11 @@ describe('NavBar 컴포넌트 테스트', () => { }); it('로그인이 되어있으면 로그아웃 버튼이 보여진다.', () => { - render(); + render(, { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); const logoutButton = screen.getByRole('button', { name: '로그아웃' }); @@ -43,7 +52,11 @@ describe('NavBar 컴포넌트 테스트', () => { }); it('로그아웃이 되어있으면 로그인 버튼이 보인다.', () => { - render(); + render(, { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); const loginButton = screen.getByRole('button', { name: '로그인' }); @@ -52,7 +65,11 @@ describe('NavBar 컴포넌트 테스트', () => { it('로그아웃 버튼을 누르면 submit으로 액션을 발생시킨다.', async () => { const user = userEvent.setup(); - render(); + render(, { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); const logoutButton = screen.getByRole('button', { name: '로그아웃' }); @@ -70,7 +87,11 @@ describe('NavBar 컴포넌트 테스트', () => { it('메뉴 버튼을 누르면 onClickMenuButton이 호출된다.', async () => { const user = userEvent.setup(); - render(); + render(, { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); const menuButton = screen.getByTestId('menu-button'); From 34fbceb353b017868f066ce1bd256f0662d1450c Mon Sep 17 00:00:00 2001 From: BHyeonKim Date: Sun, 8 Jun 2025 21:29:11 +0900 Subject: [PATCH 18/18] =?UTF-8?q?refactor:=20=EC=B2=B4=EA=B2=B0=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/root.tsx | 6 ----- src/app/routes/trade.$ticker.tsx | 4 ++++ .../trade/hooks/useTradeNotification.tsx | 22 ++----------------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/src/app/root.tsx b/src/app/root.tsx index 862b18e..59e6163 100644 --- a/src/app/root.tsx +++ b/src/app/root.tsx @@ -12,7 +12,6 @@ import type { Route } from './+types/root'; import './app.css'; import { Slide } from 'react-toastify'; -import useTradeNotification from '~/features/trade/hooks/useTradeNotification'; import StompProvider from './provider/StompProvider'; import UserIdProvider from './provider/UserInfoProvider'; @@ -76,11 +75,6 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } -function TradeNotificationHandler() { - useTradeNotification(); - return null; -} - export default function App() { return ( diff --git a/src/app/routes/trade.$ticker.tsx b/src/app/routes/trade.$ticker.tsx index 693438d..1325a3f 100644 --- a/src/app/routes/trade.$ticker.tsx +++ b/src/app/routes/trade.$ticker.tsx @@ -9,10 +9,12 @@ import { AIChatBot } from '~/features/chat'; import { CoinListWithSearchBar } from '~/features/coin-search-list'; import { OrderForm, OrderFormFallback } from '~/features/order'; import { ExecutionList } from '~/features/order-execution-list'; +import useTradeNotification from '~/features/trade/hooks/useTradeNotification'; import { Orderbook, StockChart } from '~/features/tradeview'; import Container from '~/shared/ui/Container'; import ContainerTitle from '~/shared/ui/ContainerTitle'; import { NavBar, SideBar } from '~/widgets/navbar'; +import { useUserId } from '../provider/UserInfoProvider'; import type { Route } from './+types/trade.$ticker'; export async function loader({ request, params }: Route.LoaderArgs) { @@ -41,6 +43,8 @@ export async function clientAction() { export default function TradeRouteComponent({ loaderData, }: Route.ComponentProps) { + const { userId } = useUserId(); + useTradeNotification(userId || 0); const [isMenuOpen, setIsMenuOpen] = useState(false); const { coinInfo, coinList, isLoggedIn } = loaderData; const coinListWithIcon = coinList.map((coinInfo) => ({ diff --git a/src/features/trade/hooks/useTradeNotification.tsx b/src/features/trade/hooks/useTradeNotification.tsx index 249f3e2..38ac6ae 100644 --- a/src/features/trade/hooks/useTradeNotification.tsx +++ b/src/features/trade/hooks/useTradeNotification.tsx @@ -1,9 +1,7 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { toast } from 'react-toastify/unstyled'; import { useStompClient } from '~/app/provider/StompProvider'; -import { api as userApi } from '~/entities/user'; -import type { UserInfoResponse } from '~/entities/user/types/user.type'; type TradeNotification = { ticker: string; @@ -13,24 +11,8 @@ type TradeNotification = { tradedTime: string; }; -export default function useTradeNotification() { +export default function useTradeNotification(userId: number) { const { client, connected } = useStompClient(); - const [userId, setUserId] = useState(null); - - useEffect(() => { - const fetchUserInfo = async () => { - try { - const response = await userApi.getUserInfo(); - const { data } = await (response.json() as Promise); - setUserId(data.userId); - } catch (error) { - console.error('Failed to fetch user info:', error); - toast.error('사용자 정보를 가져오는데 실패했습니다.'); - } - }; - - fetchUserInfo(); - }, []); useEffect(() => { if (!client || !connected || !userId) return;