Skip to content

Commit

Permalink
Merge pull request #104 from boostcampwm-2024/feature/connect/stockDe…
Browse files Browse the repository at this point in the history
…tail-#61

[FE] 6.06 주식 상세 정보 API 연동
  • Loading branch information
dannysir authored Nov 14, 2024
2 parents f8d35b1 + f0488b3 commit c6a30bf
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 61 deletions.
49 changes: 36 additions & 13 deletions FE/src/components/StocksDetail/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import { useEffect, useRef, useState } from 'react';
import { dummy } from './dummy';
import { TiemCategory } from 'types';
import { drawBarChart, drawCandleChart, drawLineChart } from 'utils/chart';
import { useQuery } from '@tanstack/react-query';
import { getStocksChartDataByCode } from 'service/stocks';

export default function Chart() {
const categories: { label: string; value: TiemCategory }[] = [
{ label: '일', value: 'D' },
{ label: '주', value: 'W' },
{ label: '월', value: 'M' },
{ label: '년', value: 'Y' },
];

type StocksDeatailChartProps = {
code: string;
};

export default function Chart({ code }: StocksDeatailChartProps) {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [timeCategory, setTimeCategory] = useState<TiemCategory>('D');

const categories: { label: string; value: TiemCategory }[] = [
{ label: '일', value: 'D' },
{ label: '주', value: 'W' },
{ label: '월', value: 'M' },
{ label: '년', value: 'Y' },
];
const { data, isLoading } = useQuery(
['stocksChartData', code, timeCategory],
() => getStocksChartDataByCode(code, timeCategory),
);

useEffect(() => {
if (isLoading) return;
if (!data) return;

const parent = containerRef.current;
const canvas = canvasRef.current;

Expand Down Expand Up @@ -49,12 +62,22 @@ export default function Chart() {
const chartHeight = canvas.height - padding.top - padding.bottom;
const boundary = chartHeight * 0.8; // chartHeight의 80%

const data = dummy.map((e) => e.low);
const reverseData = data.reverse();

const arr = reverseData.map((e) => +e.stck_oprc);

drawLineChart(ctx, data, 0, 0, chartWidth, boundary, padding, 0.1);
drawBarChart(ctx, dummy, 0, boundary, chartWidth, chartHeight, padding);
drawCandleChart(ctx, dummy, 0, 0, chartWidth, boundary, padding, 0.1);
}, []);
drawLineChart(ctx, arr, 0, 0, chartWidth, boundary, padding, 0.1);
drawBarChart(
ctx,
reverseData,
0,
boundary,
chartWidth,
chartHeight,
padding,
);
drawCandleChart(ctx, reverseData, 0, 0, chartWidth, boundary, padding, 0.1);
}, [timeCategory, data, isLoading]);

return (
<div
Expand Down
67 changes: 42 additions & 25 deletions FE/src/components/StocksDetail/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,55 @@
export default function Header() {
import { useQuery } from '@tanstack/react-query';
import { getStocksByCode } from 'service/stocks';

type StocksDeatailHeaderProps = {
code: string;
};

export default function Header({ code }: StocksDeatailHeaderProps) {
const { data, isLoading } = useQuery(['stocks', code], () =>
getStocksByCode(code),
);

if (isLoading) return;
if (!data) return;

const { stck_prpr, prdy_vrss, prdy_vrss_sign, prdy_ctrt, hts_avls, per } =
data;

const stockInfo: { label: string; value: string }[] = [
{ label: '시총', value: `${Number(hts_avls).toLocaleString()}억원` },
{ label: 'PER', value: `${per}배` },
];

const colorStyleBySign =
prdy_vrss_sign === '3'
? ''
: prdy_vrss_sign < '3'
? 'text-juga-red-60'
: 'text-juga-blue-40';

return (
<div className='flex items-center justify-between w-full h-16 px-2'>
<div className='flex flex-col font-semibold'>
<div className='flex gap-2 text-sm'>
<h2>삼성전자</h2>
<p className='text-juga-grayscale-200'>005930</p>
<h2>{'empty'}</h2>
<p className='text-juga-grayscale-200'>{code}</p>
</div>
<div className='flex items-center gap-2'>
<p className='text-lg'>60,900원</p>
<p className='text-lg'>{Number(stck_prpr).toLocaleString()}</p>
<p>어제보다</p>
<p className='text-juga-red-60'>+1800원 (3.0%)</p>
<p className={`${colorStyleBySign}`}>
+{Number(prdy_vrss).toLocaleString()}원 ({prdy_ctrt}%)
</p>
</div>
</div>
<div className='flex gap-4 text-xs font-semibold'>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>당기순이익</p>
<p>9조 8,143억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>영업이익</p>
<p>10조 4,439억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>매출액</p>
<p>74조 683억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>시총</p>
<p>361조 1,718억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>PER</p>
<p>14.79배</p>
</div>
{stockInfo.map((e, idx) => (
<div key={`stockdetailinfo${idx}`} className='flex gap-2'>
<p className='text-juga-grayscale-200'>{e.label}</p>
<p>{e.value}</p>
</div>
))}
</div>
</div>
);
Expand Down
10 changes: 8 additions & 2 deletions FE/src/page/StocksDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import Chart from 'components/StocksDetail/Chart';
import Header from 'components/StocksDetail/Header';
import PriceSection from 'components/StocksDetail/PriceSection';
import TradeSection from 'components/StocksDetail/TradeSection';
import { useParams } from 'react-router-dom';

export default function StocksDetail() {
const params = useParams();
const { id } = params;

if (!id) return;

return (
<div className='flex flex-col'>
<Header />
<Header code={id} />
<div className='flex h-[500px]'>
<div className='flex min-w-[850px] flex-col'>
<Chart />
<Chart code={id} />
<PriceSection />
</div>
<TradeSection />
Expand Down
24 changes: 24 additions & 0 deletions FE/src/service/stocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StockChartUnit, StockDetailType, TiemCategory } from 'types';

export async function getStocksByCode(code: string): Promise<StockDetailType> {
return fetch(`${import.meta.env.VITE_API_URL}/stocks/${code}`).then((res) =>
res.json(),
);
}

export async function getStocksChartDataByCode(
code: string,
peroid: TiemCategory = 'D',
start: string = '',
end: string = '',
): Promise<StockChartUnit[]> {
return fetch(`${import.meta.env.VITE_API_URL}/stocks/${code}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fid_input_date_1: start,
fid_input_date_2: end,
fid_period_div_code: peroid,
}),
}).then((res) => res.json());
}
21 changes: 21 additions & 0 deletions FE/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,24 @@ export type Padding = {
right: number;
bottom: number;
};

export type StockDetailType = {
hts_kor_isnm: string;
stck_shrn_iscd: string;
stck_prpr: string;
prdy_vrss: string;
prdy_vrss_sign: string;
prdy_ctrt: string;
hts_avls: string;
per: string;
};

export type StockChartUnit = {
stck_bsop_date: string;
stck_clpr: string;
stck_oprc: string;
stck_hgpr: string;
stck_lwpr: string;
acml_vol: string;
prdy_vrss_sign: string;
};
44 changes: 23 additions & 21 deletions FE/src/utils/chart.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { DummyStock } from 'components/StocksDetail/dummy';
import { Padding } from 'types';
import { Padding, StockChartUnit } from 'types';

export function drawLineChart(
ctx: CanvasRenderingContext2D,
Expand Down Expand Up @@ -37,7 +36,7 @@ export function drawLineChart(

export function drawBarChart(
ctx: CanvasRenderingContext2D,
data: DummyStock[],
data: StockChartUnit[],
x: number,
y: number,
width: number,
Expand All @@ -50,8 +49,12 @@ export function drawBarChart(

ctx.beginPath();

const yMax = Math.round(Math.max(...data.map((d) => d.volume)) * 1 + weight);
const yMin = Math.round(Math.min(...data.map((d) => d.volume)) * 1 - weight);
const yMax = Math.round(
Math.max(...data.map((d) => +d.acml_vol)) * 1 + weight,
);
const yMin = Math.round(
Math.min(...data.map((d) => +d.acml_vol)) * 1 - weight,
);

const gap = Math.floor((width / n) * 0.8);

Expand All @@ -60,9 +63,10 @@ export function drawBarChart(

data.forEach((e, i) => {
const cx = x + padding.left + (width * i) / (n - 1);
const cy = padding.top + ((height - y) * (e.volume - yMin)) / (yMax - yMin);
const cy =
padding.top + ((height - y) * (+e.acml_vol - yMin)) / (yMax - yMin);

ctx.fillStyle = e.open < e.close ? red : blue;
ctx.fillStyle = +e.stck_oprc < +e.stck_clpr ? red : blue;
ctx.fillRect(cx, height, gap, -cy);
});

Expand All @@ -71,7 +75,7 @@ export function drawBarChart(

export function drawCandleChart(
ctx: CanvasRenderingContext2D,
data: DummyStock[],
data: StockChartUnit[],
x: number,
y: number,
width: number,
Expand All @@ -83,15 +87,13 @@ export function drawCandleChart(

const n = data.length;

const yMax = Math.round(
Math.max(...data.map((d) => Math.max(d.close, d.open, d.high, d.low))) *
(1 + weight),
);
const yMin = Math.round(
Math.min(...data.map((d) => Math.max(d.close, d.open, d.high, d.low))) *
(1 - weight),
const arr = data.map((d) =>
Math.max(+d.stck_clpr, +d.stck_oprc, +d.stck_hgpr, +d.stck_lwpr),
);

const yMax = Math.round(Math.max(...arr) * (1 + weight));
const yMin = Math.round(Math.min(...arr) * (1 - weight));

const labels = getYAxisLabels(yMin, yMax);
labels.forEach((label) => {
const yPos =
Expand All @@ -114,23 +116,23 @@ export function drawCandleChart(
data.forEach((e, i) => {
ctx.beginPath();

const { open, close, high, low } = e;
const { stck_oprc, stck_clpr, stck_hgpr, stck_lwpr } = e;
const gap = Math.floor((width / n) * 0.8);
const cx = x + padding.left + (width * i) / (n - 1);

const openY =
y + padding.top + height - (height * (open - yMin)) / (yMax - yMin);
y + padding.top + height - (height * (+stck_oprc - yMin)) / (yMax - yMin);
const closeY =
y + padding.top + height - (height * (close - yMin)) / (yMax - yMin);
y + padding.top + height - (height * (+stck_clpr - yMin)) / (yMax - yMin);
const highY =
y + padding.top + height - (height * (high - yMin)) / (yMax - yMin);
y + padding.top + height - (height * (+stck_hgpr - yMin)) / (yMax - yMin);
const lowY =
y + padding.top + height - (height * (low - yMin)) / (yMax - yMin);
y + padding.top + height - (height * (+stck_lwpr - yMin)) / (yMax - yMin);

const blue = '#2175F3';
const red = '#FF3700';

if (open > close) {
if (+stck_oprc > +stck_clpr) {
ctx.fillStyle = blue;
ctx.strokeStyle = blue;
ctx.fillRect(cx, closeY, gap, openY - closeY);
Expand Down

0 comments on commit c6a30bf

Please sign in to comment.