Skip to content

Commit

Permalink
Merge pull request #174 from boostcampwm-2024/feature/layout/rank-#132
Browse files Browse the repository at this point in the history
[FE] 차트 고도화 작업 & 랭킹 레이아웃 생성.
  • Loading branch information
dongree authored Nov 21, 2024
2 parents 948720c + 006272f commit f04c674
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 13 deletions.
16 changes: 16 additions & 0 deletions FE/src/components/Rank/Nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const RankingCategory: string[] = ['일간'];

export default function Nav() {
return (
<div className='relative ml-4 flex gap-1 text-lg font-bold'>
{RankingCategory.map((category) => (
<button
key={category}
className={'relative border-b-4 border-juga-grayscale-black'}
>
{category}
</button>
))}
</div>
);
}
52 changes: 52 additions & 0 deletions FE/src/components/Rank/RankCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={
'flex items-center justify-between rounded p-2 transition-colors'
}
>
<div className='flex items-center gap-2'>
<div
className={`flex h-6 w-6 items-center justify-center rounded-full text-sm font-bold ${
ranking === 0
? 'bg-yellow-400 text-white'
: ranking === 1
? 'bg-gray-300 text-white'
: ranking === 2
? 'bg-amber-600 text-white'
: 'bg-gray-100 text-gray-600'
}`}
>
{ranking + 1}
</div>
<span className='text-sm font-medium'>{item.nickname}</span>
</div>
<div className='text-right'>
<span className='text-sm font-bold text-gray-700'>
{type === '수익률순'
? isProfitRankItem(item)
? `${item.profitRate}%`
: '0%'
: new Intl.NumberFormat('ko-KR', {
notation: 'compact',
maximumFractionDigits: 1,
}).format((item as AssetRankItemType).totalAsset) + '원'}
</span>
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions FE/src/components/Rank/RankList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={'flex flex-col gap-5'}>
<div className='w-full rounded-lg bg-white p-2 shadow-lg'>
<div className='mb-1 border-b pb-1'>
<h3 className='text-base font-bold text-gray-800'>{title}</h3>
</div>

<div className='space-y-1'>
{topRank.map((item, index) => (
<RankCard
key={`${item.nickname}-${index}`}
item={item}
ranking={index}
type={title}
/>
))}
</div>
</div>
{!isLogin && userRank !== null && typeof userRank.rank === 'number' ? (
<div className={'w-full rounded-lg bg-white px-2 pb-1 pt-2 shadow-lg'}>
<div className='border-b'>
<h3 className='text-base font-bold text-gray-800'>{`내 ${title} 순위`}</h3>
</div>

<div className={'space-y-1'}>
<RankCard item={userRank} ranking={userRank.rank} type={title} />
</div>
</div>
) : null}
</div>
);
}
4 changes: 4 additions & 0 deletions FE/src/components/Rank/RankType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TmpDataType = {
nickname: string;
value: number;
};
133 changes: 133 additions & 0 deletions FE/src/components/Rank/bummyData.ts
Original file line number Diff line number Diff line change
@@ -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등으로 설정
},
},
};
51 changes: 40 additions & 11 deletions FE/src/components/StocksDetail/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -33,6 +34,11 @@ type StocksDeatailChartProps = {
code: string;
};

export type MousePositionType = {
x: number;
y: number;
};

export default function Chart({ code }: StocksDeatailChartProps) {
const containerRef = useRef<HTMLDivElement>(null);
const upperChartCanvasRef = useRef<HTMLCanvasElement>(null);
Expand All @@ -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<MousePositionType>({
x: 0,
y: 0,
});

const { data, isLoading } = useQuery(
['stocksChartData', code, timeCategory],
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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,
);
}
},
[

Check warning on line 263 in FE/src/components/StocksDetail/Chart.tsx

View workflow job for this annotation

GitHub Actions / FE-test-and-build

React Hook useCallback has unnecessary dependencies: 'drawBarChart', 'drawCandleChart', 'drawChartGrid', 'drawLineChart', 'drawLowerYAxis', 'drawUpperYAxis', 'drawXAxis', and 'padding'. Either exclude them or remove the dependency array. Outer scope values like 'padding' aren't valid dependencies because mutating them doesn't re-render the component

Check warning on line 263 in FE/src/components/StocksDetail/Chart.tsx

View workflow job for this annotation

GitHub Actions / build-and-deploy (fe, FE, 5173, juga-docker-fe)

React Hook useCallback has unnecessary dependencies: 'drawBarChart', 'drawCandleChart', 'drawChartGrid', 'drawLineChart', 'drawLowerYAxis', 'drawUpperYAxis', 'drawXAxis', and 'padding'. Either exclude them or remove the dependency array. Outer scope values like 'padding' aren't valid dependencies because mutating them doesn't re-render the component
padding,
Expand Down Expand Up @@ -295,6 +322,7 @@ export default function Chart({ code }: StocksDeatailChartProps) {
lowerChartY.current,
chartX.current,
data,
mousePosition,
);
}, [
timeCategory,
Expand All @@ -303,6 +331,7 @@ export default function Chart({ code }: StocksDeatailChartProps) {
setCanvasSize,
renderChart,
charSizeConfig,
mousePosition,
]);

return (
Expand All @@ -324,7 +353,7 @@ export default function Chart({ code }: StocksDeatailChartProps) {
<div
ref={containerRef}
className='mt-2 flex h-[200px] w-full flex-col'
// onMouseMove={getCanvasMousePosition}
onMouseMove={getCanvasMousePosition}
>
{/* Upper 차트 영역 */}
<div className='flex flex-row'>
Expand Down
23 changes: 21 additions & 2 deletions FE/src/page/Rank.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>Loading...</div>;
const data = dummyRankData;

return (
<div>
<h1>Ranking</h1>
<div className='rounded-xl px-4'>
<div className='mb-2'>
<Nav />
</div>

<div className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
<RankList title='수익률순' data={data.profitRateRanking} />
<RankList title='자산순' data={data.assetRanking} />
</div>
</div>
);
}
Loading

0 comments on commit f04c674

Please sign in to comment.