Skip to content

Commit 3fa11ff

Browse files
committed
차트 리펙
1 parent e875724 commit 3fa11ff

File tree

14 files changed

+616
-9
lines changed

14 files changed

+616
-9
lines changed

src/app/routes/trade.$ticker.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CoinListWithSearchBar } from '~/features/coin-search-list';
1010
import { OrderForm, OrderFormFallback } from '~/features/order';
1111
import { ExecutionList } from '~/features/order-execution-list';
1212
import useTradeNotification from '~/features/trade/hooks/useTradeNotification';
13+
import Chart from '~/features/tradeview/ui/Chart';
1314
import Container from '~/shared/ui/Container';
1415
import ContainerTitle from '~/shared/ui/ContainerTitle';
1516
import { NavBar, SideBar } from '~/widgets/navbar';
@@ -82,12 +83,7 @@ export default function TradeRouteComponent({
8283
<Container>
8384
<ContainerTitle>실시간 차트</ContainerTitle>
8485
<Suspense fallback="차트데이터를 가져오고 있습니다.">
85-
{coinInfo && (
86-
<LazyStockChart
87-
key={`chart-${coinInfo.ticker}`}
88-
ticker={coinInfo.ticker}
89-
/>
90-
)}
86+
<Chart />
9187
</Suspense>
9288
</Container>
9389
</div>

src/features/tradeview/api/tradeview.endpoints.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import ApiClient from '~/shared/api/httpClient';
33
import type { RowData } from '../types/tradeview.type';
44

55
export default {
6-
getPastData: async (ticker = 'TRUMP') => {
7-
return await ApiClient.get<RowData[]>(`api/minute-ohlc?ticker=${ticker}`);
6+
getPastData: async (ticker = 'TRUMP', period = 1) => {
7+
return await ApiClient.get<RowData[]>(
8+
`api/minute-ohlc?ticker=${ticker}&period=${period}`,
9+
);
810
},
911
};
1012
/* v8 ignore end */
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as am5 from '@amcharts/amcharts5';
2+
import am5themes_Animated from '@amcharts/amcharts5/themes/Animated';
3+
import React, { useEffect, useRef, useState, type ReactNode } from 'react';
4+
5+
export type ChartContainerProps = {
6+
containerId: string;
7+
toolbarId: string;
8+
children: ReactNode;
9+
};
10+
11+
export type ChartContainer = {
12+
chartRoot: am5.Root | null;
13+
chartToolbarContainerRef: React.RefObject<HTMLDivElement | null>;
14+
};
15+
16+
export default function ChartContainer({
17+
containerId,
18+
toolbarId,
19+
children,
20+
}: ChartContainerProps) {
21+
const [chartRoot, setChartRoot] = useState<ChartContainer['chartRoot']>(null);
22+
const chartToolbarContainerRef =
23+
useRef<ChartContainer['chartToolbarContainerRef']['current']>(null);
24+
25+
const childrenWithProps = React.Children.map(children, (child) => {
26+
if (React.isValidElement<ChartContainer>(child)) {
27+
return React.cloneElement(child, { chartRoot });
28+
}
29+
return child;
30+
});
31+
32+
useEffect(() => {
33+
if (chartRoot) return;
34+
const root = am5.Root.new(containerId);
35+
36+
const Theme = am5.Theme.new(root);
37+
Theme.rule('Grid', ['scrollbar', 'minor']).setAll({
38+
visible: false,
39+
});
40+
41+
root.setThemes([am5themes_Animated.new(root), Theme]);
42+
43+
setChartRoot(root);
44+
45+
return () => {
46+
if (!chartRoot) return;
47+
root.dispose();
48+
};
49+
}, [containerId, chartRoot]);
50+
51+
return (
52+
<>
53+
<div id={toolbarId} ref={chartToolbarContainerRef} />
54+
<div id={containerId} className="h-full">
55+
{childrenWithProps}
56+
</div>
57+
</>
58+
);
59+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as am5 from '@amcharts/amcharts5';
2+
import * as am5stock from '@amcharts/amcharts5/stock';
3+
import * as am5xy from '@amcharts/amcharts5/xy';
4+
5+
import React, { type PropsWithChildren, useEffect, useState } from 'react';
6+
import type { StockChart } from './StockChart';
7+
8+
type MainPanelProps = PropsWithChildren<Partial<StockChart>>;
9+
10+
export type MainPanel = {
11+
mainPanel: am5stock.StockPanel | null;
12+
dateAxis: am5xy.GaplessDateAxis<am5xy.AxisRenderer> | null;
13+
valueAxis: am5xy.ValueAxis<am5xy.AxisRenderer> | null;
14+
valueLegend: am5stock.StockLegend | null;
15+
} & StockChart;
16+
17+
export default function MainPanel({
18+
chartRoot,
19+
stockChart,
20+
children,
21+
}: MainPanelProps) {
22+
const [mainPanel, setMainPanel] = useState<MainPanel['mainPanel']>(null);
23+
const [dateAxis, setDateAxis] = useState<MainPanel['dateAxis']>(null);
24+
const [valueAxis, setValueAxis] = useState<MainPanel['valueAxis']>(null);
25+
const [valueLegend, setValueLegend] =
26+
useState<MainPanel['valueLegend']>(null);
27+
28+
const childrenWithProps = React.Children.map(children, (child) => {
29+
if (React.isValidElement<MainPanel>(child)) {
30+
return React.cloneElement(child, {
31+
chartRoot,
32+
stockChart,
33+
mainPanel,
34+
dateAxis,
35+
valueAxis,
36+
valueLegend,
37+
});
38+
}
39+
return child;
40+
});
41+
42+
useEffect(() => {
43+
if (!chartRoot || !stockChart) return;
44+
const newPanel = stockChart.panels.push(
45+
am5stock.StockPanel.new(chartRoot, {
46+
wheelY: 'zoomX',
47+
panX: true,
48+
panY: true,
49+
}),
50+
);
51+
setMainPanel(newPanel);
52+
53+
const newDateAxis = newPanel.xAxes.push(
54+
am5xy.GaplessDateAxis.new(chartRoot, {
55+
baseInterval: {
56+
timeUnit: 'minute',
57+
count: 1,
58+
},
59+
renderer: am5xy.AxisRendererX.new(chartRoot, {
60+
minorGridEnabled: true,
61+
}),
62+
tooltip: am5.Tooltip.new(chartRoot, {}),
63+
}),
64+
);
65+
66+
setDateAxis(newDateAxis);
67+
68+
const newValueLegend = newPanel.plotContainer.children.push(
69+
am5stock.StockLegend.new(chartRoot, {
70+
stockChart,
71+
}),
72+
);
73+
74+
setValueLegend(newValueLegend);
75+
76+
const newValueAxis = newPanel.yAxes.push(
77+
am5xy.ValueAxis.new(chartRoot, {
78+
renderer: am5xy.AxisRendererY.new(chartRoot, {
79+
pan: 'zoom',
80+
}),
81+
extraMin: 0.1,
82+
tooltip: am5.Tooltip.new(chartRoot, {}),
83+
numberFormat: '#,###.00',
84+
extraTooltipPrecision: 2,
85+
}),
86+
);
87+
88+
setValueAxis(newValueAxis);
89+
90+
newPanel.set(
91+
'cursor',
92+
am5xy.XYCursor.new(chartRoot, {
93+
yAxis: newValueAxis,
94+
xAxis: newDateAxis,
95+
snapToSeries: newValueAxis.series,
96+
snapToSeriesBy: 'y!',
97+
}),
98+
);
99+
100+
return () => {
101+
newPanel.dispose();
102+
};
103+
}, [chartRoot, stockChart]);
104+
105+
return childrenWithProps;
106+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as am5xy from '@amcharts/amcharts5/xy';
2+
import { type PropsWithChildren, useEffect } from 'react';
3+
import type { SbSeries } from './SbSeries';
4+
5+
type SbDateAxisProps = PropsWithChildren<Partial<SbSeries>>;
6+
7+
export default function SbDateAxis({
8+
children,
9+
sbDateAxisRef,
10+
chartRootRef,
11+
scrollbarRef,
12+
}: SbDateAxisProps) {
13+
useEffect(() => {
14+
if (
15+
!sbDateAxisRef ||
16+
!chartRootRef ||
17+
!chartRootRef.current ||
18+
!scrollbarRef ||
19+
!scrollbarRef.current
20+
)
21+
return;
22+
23+
sbDateAxisRef.current = scrollbarRef.current.chart.xAxes.push(
24+
am5xy.GaplessDateAxis.new(chartRootRef.current, {
25+
baseInterval: {
26+
timeUnit: 'minute',
27+
count: 1,
28+
},
29+
renderer: am5xy.AxisRendererX.new(chartRootRef.current, {
30+
minorGridEnabled: true,
31+
}),
32+
}),
33+
);
34+
}, [sbDateAxisRef, chartRootRef, scrollbarRef]);
35+
36+
return children;
37+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as am5xy from '@amcharts/amcharts5/xy';
2+
import type { PropsWithChildren } from 'react';
3+
import React, { useEffect, useRef } from 'react';
4+
import type { CandlestickData } from '../../types/tradeview.type';
5+
import type { XScrollBar } from './XScrollBar';
6+
7+
type SbSeriesProps = PropsWithChildren<
8+
Partial<XScrollBar> & { pastTimeData?: CandlestickData[] }
9+
>;
10+
11+
export type SbSeries = {
12+
sbSeriesRef: React.RefObject<am5xy.LineSeries | null>;
13+
sbValueAxisRef: React.RefObject<am5xy.ValueAxis<am5xy.AxisRenderer> | null>;
14+
sbDateAxisRef: React.RefObject<am5xy.GaplessDateAxis<am5xy.AxisRenderer> | null>;
15+
} & XScrollBar;
16+
17+
export default function SbSeries({
18+
children,
19+
scrollbarRef,
20+
chartRootRef,
21+
pastTimeData,
22+
}: SbSeriesProps) {
23+
const sbSeriesRef = useRef<SbSeries['sbSeriesRef']['current']>(null);
24+
const sbValueAxisRef = useRef<SbSeries['sbValueAxisRef']['current']>(null);
25+
const sbDateAxisRef = useRef<SbSeries['sbDateAxisRef']['current']>(null);
26+
27+
const childrenWithProps = React.Children.map(children, (child) => {
28+
if (React.isValidElement<SbSeries>(child)) {
29+
return React.cloneElement(child, {
30+
sbSeriesRef,
31+
sbValueAxisRef,
32+
sbDateAxisRef,
33+
});
34+
}
35+
return child;
36+
});
37+
38+
useEffect(() => {
39+
if (!pastTimeData || !pastTimeData.length || !sbSeriesRef.current) return;
40+
console.log(pastTimeData);
41+
sbSeriesRef.current.data.setAll(pastTimeData);
42+
}, [pastTimeData]);
43+
44+
useEffect(() => {
45+
if (
46+
!scrollbarRef ||
47+
!scrollbarRef.current ||
48+
!chartRootRef ||
49+
!chartRootRef.current ||
50+
!sbDateAxisRef.current ||
51+
!sbValueAxisRef.current
52+
)
53+
return;
54+
55+
sbSeriesRef.current = scrollbarRef.current.chart.series.push(
56+
am5xy.LineSeries.new(chartRootRef.current, {
57+
valueYField: 'Close',
58+
valueXField: 'Timestamp',
59+
xAxis: sbDateAxisRef.current,
60+
yAxis: sbValueAxisRef.current,
61+
}),
62+
);
63+
64+
sbSeriesRef.current.fills.template.setAll({
65+
visible: true,
66+
fillOpacity: 0.3,
67+
});
68+
}, [scrollbarRef, chartRootRef]);
69+
70+
return childrenWithProps;
71+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as am5xy from '@amcharts/amcharts5/xy';
2+
import { type PropsWithChildren, useEffect } from 'react';
3+
import type { SbSeries } from './SbSeries';
4+
5+
type SbValueAxisProps = PropsWithChildren<Partial<SbSeries>>;
6+
7+
export default function SbValueAxis({
8+
children,
9+
sbValueAxisRef,
10+
chartRootRef,
11+
scrollbarRef,
12+
}: SbValueAxisProps) {
13+
useEffect(() => {
14+
if (
15+
!sbValueAxisRef ||
16+
!chartRootRef ||
17+
!chartRootRef.current ||
18+
!scrollbarRef ||
19+
!scrollbarRef.current
20+
)
21+
return;
22+
23+
sbValueAxisRef.current = scrollbarRef.current.chart.yAxes.push(
24+
am5xy.ValueAxis.new(chartRootRef.current, {
25+
renderer: am5xy.AxisRendererY.new(chartRootRef.current, {}),
26+
}),
27+
);
28+
}, [sbValueAxisRef, chartRootRef, scrollbarRef]);
29+
30+
return children;
31+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as am5stock from '@amcharts/amcharts5/stock';
2+
import React, { useEffect, useState, type PropsWithChildren } from 'react';
3+
4+
import type { ChartContainer } from './ChartContainer';
5+
6+
type ChartPropsWithChildren = PropsWithChildren<
7+
Partial<
8+
ChartContainer & {
9+
settings: am5stock.IStockChartSettings;
10+
}
11+
>
12+
>;
13+
14+
export type StockChart = {
15+
stockChart: am5stock.StockChart | null;
16+
} & ChartContainer;
17+
18+
export default function StockChart({
19+
chartRoot,
20+
settings = {},
21+
children,
22+
}: ChartPropsWithChildren) {
23+
const [stockChart, setStockChart] = useState<StockChart['stockChart']>(null);
24+
25+
const childrenWithProps = React.Children.map(children, (child) => {
26+
if (React.isValidElement<StockChart>(child)) {
27+
return React.cloneElement(child, { chartRoot, stockChart });
28+
}
29+
return child;
30+
});
31+
32+
useEffect(() => {
33+
if (!chartRoot || stockChart) return;
34+
const schart = chartRoot.container.children.push(
35+
am5stock.StockChart.new(chartRoot, settings),
36+
);
37+
setStockChart(schart);
38+
39+
return () => {
40+
if (!schart) return;
41+
schart.dispose();
42+
};
43+
}, [stockChart, chartRoot, settings]);
44+
45+
return childrenWithProps;
46+
}

0 commit comments

Comments
 (0)