Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" >> .env
echo "VITE_STOMP_URL=${{ secrets.VITE_STOMP_URL }}" >> .env
echo "VITE_AI_URL=${{ secrets.VITE_AI_URL }}" >> .env
echo "VITE_SENTRY_ORG=${{ secrets.VITE_SENTRY_ORG }}" >> .env
echo "VITE_SENTRY_PROJECT=${{ secrets.VITE_SENTRY_PROJECT }}" >> .env
echo "VITE_SENTRY_AUTH_TOKEN=${{ secrets.VITE_SENTRY_AUTH_TOKEN }}" >> .env

- name: Build image
run: |
Expand Down
13 changes: 13 additions & 0 deletions instrument.server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import * as Sentry from '@sentry/react-router';

Sentry.init({
dsn: 'https://8343c6ee467e6f35f22c570a68cd2e6e@o4509544992407552.ingest.us.sentry.io/4509548888391680',
sendDefaultPii: true,
// Enable logs to be sent to Sentry
_experiments: { enableLogs: true },

integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0, // Capture 100% of the transactions
profilesSampleRate: 1.0, // profile every transaction
});
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "react-router dev",
"build": "react-router build",
"dev": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router dev",
"build": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc",
"test": "vitest",
Expand All @@ -21,12 +21,17 @@
"@react-router/fs-routes": "^7.5.3",
"@react-router/node": "^7.5.3",
"@react-router/serve": "^7.5.3",
"@sentry/profiling-node": "^9.30.0",
"@sentry/react-router": "^9.30.0",
"@stomp/stompjs": "^7.1.1",
"@xstate/react": "^5.0.4",
"clsx": "^2.1.1",
"cookie": "^1.0.2",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"isbot": "^5",
"ky": "^1.8.1",
"lightweight-charts": "^5.0.7",
"motion": "^12.12.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
Expand Down Expand Up @@ -63,8 +68,6 @@
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"msw": {
"workerDirectory": [
"public"
]
"workerDirectory": ["public"]
}
}
4 changes: 4 additions & 0 deletions react-router.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { Config } from '@react-router/dev/config';
import { sentryOnBuildEnd } from '@sentry/react-router';

export default {
appDirectory: './src/app',
ssr: true,
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
await sentryOnBuildEnd({viteConfig, reactRouterConfig, buildManifest});
},
} satisfies Config;
35 changes: 28 additions & 7 deletions src/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
import * as Sentry from '@sentry/react-router';
/* v8 ignore start */
import { StrictMode, startTransition } from 'react';
import { startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { HydratedRouter } from 'react-router/dom';

Sentry.init({
dsn: 'https://8343c6ee467e6f35f22c570a68cd2e6e@o4509544992407552.ingest.us.sentry.io/4509548888391680',

sendDefaultPii: true,

integrations: [
Sentry.reactRouterTracingIntegration(),
Sentry.replayIntegration(),
Sentry.feedbackIntegration({
colorScheme: 'system',
}),
],

_experiments: { enableLogs: true },

tracesSampleRate: 1.0,

// Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled
tracePropagationTargets: [/^\//, /^https:\/\/investfuture\.my\/api/],
// Capture Replay for 10% of all sessions,
// plus 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});

async function prepareApp() {
return Promise.resolve();
}

prepareApp().then(() => {
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>,
);
hydrateRoot(document, <HydratedRouter />);
});
});

Expand Down
9 changes: 7 additions & 2 deletions src/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import { PassThrough } from 'node:stream';

import { createReadableStreamFromReadable } from '@react-router/node';
import {
getMetaTagTransformer,
wrapSentryHandleRequest,
} from '@sentry/react-router';
import { isbot } from 'isbot';
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
Expand All @@ -10,7 +14,7 @@ import { ServerRouter } from 'react-router';

export const streamTimeout = 5_000;

export default function handleRequest(
function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
Expand Down Expand Up @@ -47,7 +51,7 @@ export default function handleRequest(
}),
);

pipe(body);
pipe(getMetaTagTransformer(body));
},
onShellError(error: unknown) {
reject(error);
Expand All @@ -71,4 +75,5 @@ export default function handleRequest(
});
}

export default wrapSentryHandleRequest(handleRequest);
/* v8 ignore end */
16 changes: 10 additions & 6 deletions src/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as Sentry from '@sentry/react-router';
import { preload } from 'react-dom';
import {
Links,
Meta,
Expand All @@ -6,12 +8,11 @@ import {
ScrollRestoration,
isRouteErrorResponse,
} from 'react-router';
import { Slide } from 'react-toastify';
import { ToastContainer } from 'react-toastify/unstyled';
import type { Route } from './+types/root';

import './app.css';
import { preload } from 'react-dom';
import { Slide } from 'react-toastify';
import type { Route } from './+types/root';
import StompProvider from './provider/StompProvider';
import UserIdProvider from './provider/UserInfoProvider';

Expand Down Expand Up @@ -121,9 +122,12 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
error.status === 404
? 'The requested page could not be found.'
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
} else if (error && error instanceof Error) {
Sentry.captureException(error);
if (import.meta.env.DEV) {
details = error.message;
stack = error.stack;
}
}

return (
Expand Down
24 changes: 12 additions & 12 deletions src/app/routes/trade.$ticker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as cookie from 'cookie';
import { AnimatePresence } from 'motion/react';
import { Suspense, lazy, useState } from 'react';
import { Suspense, lazy, useMemo, useState } from 'react';
import { Outlet, redirect } from 'react-router';

import { CoinPriceWithName, api as coinApi } from '~/entities/coin';
Expand Down Expand Up @@ -49,11 +49,16 @@ export default function TradeRouteComponent({
useTradeNotification(userId ?? 0);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { coinInfo, coinList, isLoggedIn } = loaderData;
const coinListWithIcon = coinList.map((coinInfo) => ({
...coinInfo,
coinIcon: <span>🪙</span>,
to: `/trade/${coinInfo.ticker}`,
}));
const coinListWithIcon = useMemo(
() =>
coinList.map((coinInfo) => ({
...coinInfo,
coinIcon: <span>🪙</span>,
to: `/trade/${coinInfo.ticker}`,
onClick: () => setIsMenuOpen(false),
})),
[coinList],
);

const handleOpenMenu = () => {
setIsMenuOpen(true);
Expand Down Expand Up @@ -82,12 +87,7 @@ export default function TradeRouteComponent({
<Container>
<ContainerTitle>실시간 차트</ContainerTitle>
<Suspense fallback="차트데이터를 가져오고 있습니다.">
{coinInfo && (
<LazyStockChart
key={`chart-${coinInfo.ticker}`}
ticker={coinInfo.ticker}
/>
)}
<LazyStockChart ticker={coinInfo?.ticker || 'BTC'} />
</Suspense>
</Container>
</div>
Expand Down
61 changes: 32 additions & 29 deletions src/features/coin-search-list/ui/CoinListItem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, type LinkProps } from 'react-router';
import { type LinkProps, useNavigate } from 'react-router';

import {
CoinWithIconAndName,
Expand All @@ -9,48 +9,51 @@ import { formatCurrencyKR } from '~/shared/utils';

export type CoinListItemProps = {
to: LinkProps['to'];
onClick?: () => void;
} & CoinWithIconAndNameProps;

export default function CoinListItem({
name,
ticker,
coinIcon: CoinIcon,
to,
onClick,
}: Readonly<CoinListItemProps>) {
const navigate = useNavigate();
const currentPriceData = useCurrentPrice(ticker);
const isBull = currentPriceData && currentPriceData.changeRate > 0;
const formatedPrice = `${formatCurrencyKR(
+(currentPriceData?.currentPrice ?? 0).toFixed(2),
)}원`;

const handleClickCoinItem = async () => {
onClick?.();
await navigate(to);
};

return (
<Link to={to} className="block px-2">
<button
type="button"
className="flex w-[max(300px,100%)] cursor-pointer items-center py-1"
>
<div className="flex-1">
<CoinWithIconAndName
name={name}
ticker={ticker}
coinIcon={CoinIcon}
/>
</div>
<div className="flex-1 text-right text-sm">
<span className={isBull ? 'text-red-600' : 'text-blue-700'}>
{formatedPrice}
</span>
</div>
<div className="flex-1 text-right text-sm">
<span className={isBull ? 'text-red-600' : 'text-blue-700'}>
{(currentPriceData?.changeRate ?? 0).toFixed(2)}%
</span>
</div>
<div className="flex-1 text-right text-sm">
{/* TODO: 거래량 API가 나오면 추가할 것 */}
<span>{0}</span>
</div>
</button>
</Link>
<button
type="button"
className="flex w-[max(300px,100%)] cursor-pointer items-center py-1"
onClick={handleClickCoinItem}
>
<div className="flex-1">
<CoinWithIconAndName name={name} ticker={ticker} coinIcon={CoinIcon} />
</div>
<div className="flex-1 text-right text-sm">
<span className={isBull ? 'text-red-600' : 'text-blue-700'}>
{formatedPrice}
</span>
</div>
<div className="flex-1 text-right text-sm">
<span className={isBull ? 'text-red-600' : 'text-blue-700'}>
{(currentPriceData?.changeRate ?? 0).toFixed(2)}%
</span>
</div>
<div className="flex-1 text-right text-sm">
{/* TODO: 거래량 API가 나오면 추가할 것 */}
<span>{0}</span>
</div>
</button>
);
}
13 changes: 10 additions & 3 deletions src/features/tradeview/api/tradeview.endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/* v8 ignore start */
import ApiClient from '~/shared/api/httpClient';
import type { RowData } from '../types/tradeview.type';
import type { RawData } from '../types/tradeview.type';

export default {
getPastData: async (ticker = 'TRUMP') => {
return await ApiClient.get<RowData[]>(`api/minute-ohlc?ticker=${ticker}`);
getPastData: async (
ticker = 'TRUMP',
interval = 1,
count = 100,
from?: string,
) => {
return await ApiClient.get<RawData[]>(
`api/minute-ohlc?ticker=${ticker}&count=${count}&interval=${interval}${from ? `&from=${from}` : ''}`,
);
},
};
/* v8 ignore end */
9 changes: 9 additions & 0 deletions src/features/tradeview/const/chart.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const INTERVALS = [
{ interval: 1, text: '1분' },
{ interval: 3, text: '3분' },
{ interval: 5, text: '5분' },
{ interval: 15, text: '15분' },
{ interval: 30, text: '30분' },
];

export const INTERVAL_SELECTOR_HEIGHT = 26;
Loading
Loading