diff --git a/FE/src/components/Rank/Nav.tsx b/FE/src/components/Rank/Nav.tsx new file mode 100644 index 00000000..67138e12 --- /dev/null +++ b/FE/src/components/Rank/Nav.tsx @@ -0,0 +1,16 @@ +const RankingCategory: string[] = ['일간']; + +export default function Nav() { + return ( +
+ {RankingCategory.map((category) => ( + + ))} +
+ ); +} diff --git a/FE/src/components/Rank/RankCard.tsx b/FE/src/components/Rank/RankCard.tsx new file mode 100644 index 00000000..3e74496f --- /dev/null +++ b/FE/src/components/Rank/RankCard.tsx @@ -0,0 +1,52 @@ +import { AssetRankItemType, ProfitRankItemType } from './bummyData.ts'; + +type Props = { + item: ProfitRankItemType | AssetRankItemType; + ranking: number; + type: '수익률순' | '자산순'; +}; + +export default function RankCard({ item, ranking, type }: Props) { + const isProfitRankItem = ( + item: ProfitRankItemType | AssetRankItemType, + ): item is ProfitRankItemType => { + return 'profitRate' in item; + }; + + return ( +
+
+
+ {ranking + 1} +
+ {item.nickname} +
+
+ + {type === '수익률순' + ? isProfitRankItem(item) + ? `${item.profitRate}%` + : '0%' + : new Intl.NumberFormat('ko-KR', { + notation: 'compact', + maximumFractionDigits: 1, + }).format((item as AssetRankItemType).totalAsset) + '원'} + +
+
+ ); +} diff --git a/FE/src/components/Rank/RankList.tsx b/FE/src/components/Rank/RankList.tsx new file mode 100644 index 00000000..3a09fe35 --- /dev/null +++ b/FE/src/components/Rank/RankList.tsx @@ -0,0 +1,44 @@ +import RankCard from './RankCard'; +import useAuthStore from '../../store/authStore.ts'; +import { AssetRankingType, ProfitRankingType } from './bummyData.ts'; + +type Props = { + title: '수익률순' | '자산순'; + data: ProfitRankingType | AssetRankingType; +}; + +export default function RankList({ title, data }: Props) { + const { topRank, userRank } = data; + const { isLogin } = useAuthStore(); + return ( +
+
+
+

{title}

+
+ +
+ {topRank.map((item, index) => ( + + ))} +
+
+ {!isLogin && userRank !== null && typeof userRank.rank === 'number' ? ( +
+
+

{`내 ${title} 순위`}

+
+ +
+ +
+
+ ) : null} +
+ ); +} diff --git a/FE/src/components/Rank/RankType.ts b/FE/src/components/Rank/RankType.ts new file mode 100644 index 00000000..bba826fa --- /dev/null +++ b/FE/src/components/Rank/RankType.ts @@ -0,0 +1,4 @@ +export type TmpDataType = { + nickname: string; + value: number; +}; diff --git a/FE/src/components/Rank/bummyData.ts b/FE/src/components/Rank/bummyData.ts new file mode 100644 index 00000000..acf27717 --- /dev/null +++ b/FE/src/components/Rank/bummyData.ts @@ -0,0 +1,133 @@ +// 타입 수정 +// 수익률 랭킹 타입 +export type ProfitRankItemType = { + nickname: string; + profitRate: number; + rank?: number; +}; + +// 수익률 랭킹 타입 +export type ProfitRankingType = { + topRank: ProfitRankItemType[]; + userRank: ProfitRankItemType; +}; + +// 자산 랭킹 타입 +export type AssetRankItemType = { + nickname: string; + totalAsset: number; + rank?: number; +}; + +// 자산 랭킹 타입 +export type AssetRankingType = { + topRank: AssetRankItemType[]; + userRank: AssetRankItemType; +}; + +export type RankDataType = { + profitRateRanking: ProfitRankingType; + assetRanking: AssetRankingType; +}; + +// 더미 데이터 +export const dummyRankData: RankDataType = { + profitRateRanking: { + topRank: [ + { + nickname: '투자의신', + profitRate: 356.72, + }, + { + nickname: '주식왕', + profitRate: 245.89, + }, + { + nickname: '워렌버핏', + profitRate: 198.45, + }, + { + nickname: '존버마스터', + profitRate: 156.23, + }, + { + nickname: '주린이탈출', + profitRate: 134.51, + }, + { + nickname: '테슬라홀더', + profitRate: 122.34, + }, + { + nickname: '배당투자자', + profitRate: 98.67, + }, + { + nickname: '단타치는무도가', + profitRate: 87.91, + }, + { + nickname: '가치투자자', + profitRate: 76.45, + }, + { + nickname: '코스피불독', + profitRate: 65.23, + }, + ], + userRank: { + nickname: '나의닉네임', + profitRate: 45.67, + rank: 23, // 23등으로 설정 + }, + }, + assetRanking: { + topRank: [ + { + nickname: '자산왕', + totalAsset: 15800000000, + }, + { + nickname: '억만장자', + totalAsset: 9200000000, + }, + { + nickname: '주식부자', + totalAsset: 7500000000, + }, + { + nickname: '연봉1억', + totalAsset: 6300000000, + }, + { + nickname: '월급쟁이탈출', + totalAsset: 4800000000, + }, + { + nickname: '부자될사람', + totalAsset: 3200000000, + }, + { + nickname: '재테크고수', + totalAsset: 2500000000, + }, + { + nickname: '천만원돌파', + totalAsset: 1800000000, + }, + { + nickname: '주식으로퇴사', + totalAsset: 1200000000, + }, + { + nickname: '투자의시작', + totalAsset: 950000000, + }, + ], + userRank: { + nickname: '나의닉네임', + totalAsset: 850000000, + rank: 15, // 15등으로 설정 + }, + }, +}; diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index b14a28d8..51f6aae7 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -14,6 +14,7 @@ import { drawXAxis } from 'utils/chart/drawXAxis.ts'; import { drawUpperYAxis } from 'utils/chart/drawUpperYAxis.ts'; import { drawLowerYAxis } from 'utils/chart/drawLowerYAxis.ts'; import { drawChartGrid } from 'utils/chart/drawChartGrid.ts'; +import { drawMouseGrid } from '../../utils/chart/drawMouseGrid.ts'; const categories: { label: string; value: TiemCategory }[] = [ { label: '일', value: 'D' }, @@ -33,6 +34,11 @@ type StocksDeatailChartProps = { code: string; }; +export type MousePositionType = { + x: number; + y: number; +}; + export default function Chart({ code }: StocksDeatailChartProps) { const containerRef = useRef(null); const upperChartCanvasRef = useRef(null); @@ -52,6 +58,10 @@ export default function Chart({ code }: StocksDeatailChartProps) { const [isDragging, setIsDragging] = useState(false); const [upperLabelNum, setUpperLabelNum] = useState(3); const [lowerLabelNum, setLowerLabelNum] = useState(3); + const [mousePosition, setMousePosition] = useState({ + x: 0, + y: 0, + }); const { data, isLoading } = useQuery( ['stocksChartData', code, timeCategory], @@ -105,16 +115,14 @@ export default function Chart({ code }: StocksDeatailChartProps) { setIsDragging(false); }, []); - // const getCanvasMousePosition = (e: MouseEvent) => { - // if (!containerRef.current) return; - // const rect = containerRef.current.getBoundingClientRect(); - // const tmp = { - // x:e.clientX - rect.left, - // y: e.clientY - rect.top, - // }; - // console.log(tmp); - // return tmp; - // }; + const getCanvasMousePosition = (e: MouseEvent) => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + setMousePosition({ + x: (e.clientX - rect.left) * 2, + y: (e.clientY - rect.top) * 2, + }); + }; useEffect(() => { if (isDragging) { @@ -147,6 +155,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { lowerChartYCanvas: HTMLCanvasElement, chartXCanvas: HTMLCanvasElement, chartData: StockChartUnit[], + mousePosition: MousePositionType, ) => { const UpperChartCtx = upperChartCanvas.getContext('2d'); const LowerChartCtx = lowerChartCanvas.getContext('2d'); @@ -232,6 +241,24 @@ export default function Chart({ code }: StocksDeatailChartProps) { chartXCanvas.height, padding, ); + + if ( + mousePosition.x > padding.left && + mousePosition.x < upperChartCanvas.width && + mousePosition.y > padding.top && + mousePosition.y < upperChartCanvas.height + lowerChartCanvas.height + ) { + drawMouseGrid( + UpperChartCtx, + upperChartCanvas.width - padding.left - padding.right, + upperChartCanvas.height - padding.top - padding.bottom, + LowerChartCtx, + lowerChartCanvas.width - padding.left - padding.right, + lowerChartCanvas.height - padding.top - padding.bottom, + padding, + mousePosition, + ); + } }, [ padding, @@ -295,6 +322,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { lowerChartY.current, chartX.current, data, + mousePosition, ); }, [ timeCategory, @@ -303,6 +331,7 @@ export default function Chart({ code }: StocksDeatailChartProps) { setCanvasSize, renderChart, charSizeConfig, + mousePosition, ]); return ( @@ -324,7 +353,7 @@ export default function Chart({ code }: StocksDeatailChartProps) {
{/* Upper 차트 영역 */}
diff --git a/FE/src/page/Rank.tsx b/FE/src/page/Rank.tsx index 4406032d..6c13c715 100644 --- a/FE/src/page/Rank.tsx +++ b/FE/src/page/Rank.tsx @@ -1,7 +1,26 @@ +import Nav from 'components/Rank/Nav.tsx'; +import RankList from '../components/Rank/RankList.tsx'; +import { dummyRankData } from '../components/Rank/bummyData.ts'; + export default function Rank() { + // const { data, isLoading } = useQuery({ + // queryKey: ['Rank'], + // queryFn: () => getRanking(), + // }); + // + // if (isLoading) return
Loading...
; + const data = dummyRankData; + return ( -
-

Ranking

+
+
+
+ +
+ + +
); } diff --git a/FE/src/service/getRanking.ts b/FE/src/service/getRanking.ts new file mode 100644 index 00000000..2c5221d9 --- /dev/null +++ b/FE/src/service/getRanking.ts @@ -0,0 +1,7 @@ +export const getRanking = async () => { + const response = await fetch(`${import.meta.env.VITE_API_URL}/ranking`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); +}; diff --git a/FE/src/utils/chart/drawMouseGrid.ts b/FE/src/utils/chart/drawMouseGrid.ts new file mode 100644 index 00000000..01d55441 --- /dev/null +++ b/FE/src/utils/chart/drawMouseGrid.ts @@ -0,0 +1,81 @@ +import { Padding } from 'types.ts'; +import { MousePositionType } from 'components/StocksDetail/Chart.tsx'; + +export const drawMouseGrid = ( + upperChartCtx: CanvasRenderingContext2D, + upperChartWidth: number, + upperChartHeight: number, + lowerChartCtx: CanvasRenderingContext2D, + lowerChartWidth: number, + lowerChartHeight: number, + padding: Padding, + mousePosition: MousePositionType, +) => { + if ( + mousePosition.x > 0 && + mousePosition.x < upperChartWidth + padding.left + padding.right + ) { + upperChartCtx.beginPath(); + upperChartCtx.setLineDash([10, 10]); + upperChartCtx.moveTo(mousePosition.x, padding.top); + upperChartCtx.lineTo( + mousePosition.x, + padding.top + upperChartHeight + padding.bottom, + ); + + upperChartCtx.strokeStyle = '#6E8091'; + upperChartCtx.lineWidth = 1; + upperChartCtx.stroke(); + + lowerChartCtx.beginPath(); + lowerChartCtx.setLineDash([10, 10]); + lowerChartCtx.moveTo(mousePosition.x, 0); + lowerChartCtx.lineTo(mousePosition.x, upperChartHeight + padding.bottom); + + lowerChartCtx.strokeStyle = '#6E8091'; + lowerChartCtx.lineWidth = 1; + lowerChartCtx.stroke(); + } + + if ( + mousePosition.y > 0 && + mousePosition.y < upperChartHeight + padding.top + padding.bottom + ) { + upperChartCtx.beginPath(); + upperChartCtx.moveTo(0, mousePosition.y); + upperChartCtx.lineTo( + upperChartWidth + padding.left + padding.right, + mousePosition.y, + ); + + upperChartCtx.strokeStyle = '#6E8091'; + upperChartCtx.lineWidth = 1; + upperChartCtx.stroke(); + } + + if ( + mousePosition.y > upperChartHeight + padding.top + padding.bottom && + mousePosition.y < + upperChartHeight + + padding.top + + padding.bottom + + lowerChartHeight + + padding.top + + padding.bottom + ) { + lowerChartCtx.beginPath(); + lowerChartCtx.moveTo( + 0, + mousePosition.y - (upperChartHeight + padding.top + padding.bottom), + ); + lowerChartCtx.lineTo( + lowerChartWidth + padding.left + padding.right, + mousePosition.y - + (upperChartHeight + padding.top + padding.bottom + padding.bottom), + ); + + lowerChartCtx.strokeStyle = '#6E8091'; + lowerChartCtx.lineWidth = 1; + lowerChartCtx.stroke(); + } +};