From f99f642a85e2396211d647133435894bd7933ced Mon Sep 17 00:00:00 2001 From: dongree Date: Tue, 12 Nov 2024 16:40:39 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D=20?= =?UTF-8?q?=EC=84=A0=EC=B0=A8=ED=8A=B8,=20=EA=B1=B0=EB=9E=98=EB=9F=89=20?= =?UTF-8?q?=EB=A7=89=EB=8C=80=20=EC=B0=A8=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#60?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StocksDetail/Chart.tsx | 122 ++++- FE/src/components/StocksDetail/dummy.ts | 611 +++++++++++++++++++++++ 2 files changed, 732 insertions(+), 1 deletion(-) create mode 100644 FE/src/components/StocksDetail/dummy.ts diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index be34074b..337169ad 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -1,3 +1,123 @@ +import { useEffect, useRef } from 'react'; +import { dummy, DummyStock } from './dummy'; + +type Padding = { + top: number; + left: number; + right: number; + bottom: number; +}; + export default function Chart() { - return
차트
; + const containerRef = useRef(null); + const canvasRef = useRef(null); + + useEffect(() => { + const parent = containerRef.current; + const canvas = canvasRef.current; + + if (!canvas || !parent) return; + + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + ctx.fillStyle = 'white'; + + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const padding = { + top: 10, + right: 10, + bottom: 10, + left: 10, + }; + + const chartWidth = canvas.width - padding.left - padding.right; + const chartHeight = canvas.height - padding.top - padding.bottom; + const volumeBoundary = chartHeight * 0.2; // chartHeight의 20% + const mainHeight = chartHeight - volumeBoundary; + + drawLineChart(ctx, dummy, chartWidth, mainHeight, padding); + + drawBarChart( + ctx, + dummy, + chartWidth, + chartHeight, + padding, + 0, + chartHeight * 0.8, + ); + }, []); + + return ( +
+ +
+ ); +} + +function drawLineChart( + ctx: CanvasRenderingContext2D, + data: DummyStock[], + width: number, + height: number, + padding: Padding, + x: number = 0, + y: number = 0, +) { + ctx.beginPath(); + + const n = data.length; + + const yMax = Math.round(Math.max(...data.map((d) => d.low)) * 1.006 * 100); + const yMin = Math.round(Math.min(...data.map((d) => d.low)) * 0.994 * 100); + + data.forEach((v, i) => { + const value = Math.round(v.low * 100); + const cx = x + padding.left + (width * i) / (n - 1); + const cy = + y + padding.top + height - (height * (value - yMin)) / (yMax - yMin); + + if (i === 0) { + ctx.moveTo(cx, cy); + } else { + ctx.lineTo(cx, cy); + } + }); + + ctx.lineWidth = 1; + ctx.stroke(); +} + +function drawBarChart( + ctx: CanvasRenderingContext2D, + data: DummyStock[], + width: number, + height: number, + padding: Padding, + x: number, + y: number, +) { + ctx.beginPath(); + + const yMax = Math.round(Math.max(...data.map((d) => d.volume)) * 1.006 * 100); + const yMin = Math.round(Math.min(...data.map((d) => d.volume)) * 0.994 * 100); + + const gap = Math.floor((width / dummy.length) * 0.8); + + data.forEach((e, i) => { + const value = Math.round(e.volume * 100); + const cx = x + padding.left + (width * i) / (dummy.length - 1); + const cy = ((height - y) * (value - yMin)) / (yMax - yMin); + + ctx.fillStyle = e.open < e.close ? 'red' : 'blue'; + ctx.fillRect(cx, height, gap, -cy); + }); + + ctx.lineWidth = 2; + ctx.stroke(); } diff --git a/FE/src/components/StocksDetail/dummy.ts b/FE/src/components/StocksDetail/dummy.ts new file mode 100644 index 00000000..ec2288da --- /dev/null +++ b/FE/src/components/StocksDetail/dummy.ts @@ -0,0 +1,611 @@ +export type DummyStock = { + date: string; + open: number; + close: number; + high: number; + low: number; + volume: number; +}; + +export const dummy: DummyStock[] = [ + { + date: '2024-11-07', + open: 58000, + close: 57000, + high: 58300, + low: 57000, + volume: 13451844, + }, + { + date: '2024-11-06', + open: 56900, + close: 57500, + high: 58100, + low: 56800, + volume: 16951844, + }, + { + date: '2024-11-05', + open: 57600, + close: 57300, + high: 58000, + low: 56300, + volume: 21901844, + }, + { + date: '2024-11-04', + open: 57800, + close: 57600, + high: 58100, + low: 57200, + volume: 17291844, + }, + { + date: '2024-11-03', + open: 58600, + close: 58700, + high: 59400, + low: 58400, + volume: 15471844, + }, + { + date: '2024-10-31', + open: 59000, + close: 58300, + high: 59600, + low: 58100, + volume: 18831844, + }, + { + date: '2024-10-30', + open: 58500, + close: 59200, + high: 61200, + low: 58300, + volume: 34901844, + }, + { + date: '2024-10-29', + open: 59100, + close: 59100, + high: 59800, + low: 58600, + volume: 19181844, + }, + { + date: '2024-10-28', + open: 58000, + close: 59600, + high: 59600, + low: 57300, + volume: 27871844, + }, + { + date: '2024-10-27', + open: 55700, + close: 58100, + high: 58500, + low: 55700, + volume: 27571844, + }, + { + date: '2024-10-24', + open: 56000, + close: 55900, + high: 56900, + low: 55800, + volume: 25511844, + }, + { + date: '2024-10-23', + open: 58200, + close: 56600, + high: 58500, + low: 56600, + volume: 30701844, + }, + { + date: '2024-10-22', + open: 57500, + close: 59100, + high: 60000, + low: 57100, + volume: 27111844, + }, + { + date: '2024-10-21', + open: 58800, + close: 57700, + high: 58900, + low: 57700, + volume: 26881844, + }, + { + date: '2024-10-20', + open: 59000, + close: 59000, + high: 59600, + low: 58500, + volume: 18331844, + }, + { + date: '2024-10-17', + open: 59900, + close: 59200, + high: 60100, + low: 59100, + volume: 14171844, + }, + { + date: '2024-10-16', + open: 59400, + close: 59700, + high: 60100, + low: 59100, + volume: 23001844, + }, + { + date: '2024-10-15', + open: 59400, + close: 59500, + high: 60000, + low: 59200, + volume: 23021844, + }, + { + date: '2024-10-14', + open: 61100, + close: 61000, + high: 61400, + low: 60100, + volume: 20651844, + }, + { + date: '2024-10-13', + open: 59500, + close: 60800, + high: 61200, + low: 59400, + volume: 20371844, + }, + { + date: '2024-10-10', + open: 59100, + close: 59300, + high: 60100, + low: 59000, + volume: 28901844, + }, + { + date: '2024-10-09', + open: 60100, + close: 58900, + high: 60200, + low: 58900, + volume: 44601844, + }, + { + date: '2024-10-08', + open: 60000, + close: 60100, + high: 60500, + low: 59800, + volume: 18631844, + }, + { + date: '2024-10-07', + open: 59800, + close: 60000, + high: 60300, + low: 59500, + volume: 17651844, + }, + { + date: '2024-10-06', + open: 59500, + close: 59800, + high: 60000, + low: 59300, + volume: 16671844, + }, + { + date: '2024-10-03', + open: 59000, + close: 59200, + high: 59500, + low: 58800, + volume: 16671844, + }, + { + date: '2024-10-02', + open: 59500, + close: 59000, + high: 59800, + low: 58800, + volume: 17651844, + }, + { + date: '2024-10-01', + open: 60000, + close: 59500, + high: 60200, + low: 59300, + volume: 18631844, + }, + { + date: '2024-09-30', + open: 60500, + close: 60000, + high: 60800, + low: 59800, + volume: 19611844, + }, + { + date: '2024-09-27', + open: 61000, + close: 60500, + high: 61200, + low: 60300, + volume: 20591844, + }, + { + date: '2024-09-26', + open: 61500, + close: 61000, + high: 61800, + low: 60800, + volume: 21571844, + }, + { + date: '2024-09-25', + open: 62000, + close: 61500, + high: 62300, + low: 61300, + volume: 22551844, + }, + { + date: '2024-09-24', + open: 62500, + close: 62000, + high: 62800, + low: 61800, + volume: 23531844, + }, + { + date: '2024-09-23', + open: 63000, + close: 62500, + high: 63300, + low: 62300, + volume: 24511844, + }, + { + date: '2024-09-20', + open: 63500, + close: 63000, + high: 63800, + low: 62800, + volume: 25491844, + }, + { + date: '2024-09-19', + open: 64000, + close: 63500, + high: 64300, + low: 63300, + volume: 26471844, + }, + { + date: '2024-09-18', + open: 64500, + close: 64000, + high: 64800, + low: 63800, + volume: 27451844, + }, + { + date: '2024-09-17', + open: 65000, + close: 64500, + high: 65300, + low: 64300, + volume: 28431844, + }, + { + date: '2024-09-16', + open: 65500, + close: 65000, + high: 65800, + low: 64800, + volume: 29411844, + }, + { + date: '2024-09-13', + open: 66000, + close: 65500, + high: 66300, + low: 65300, + volume: 30391844, + }, + { + date: '2024-09-12', + open: 66500, + close: 66000, + high: 66800, + low: 65800, + volume: 31371844, + }, + { + date: '2024-09-11', + open: 67000, + close: 66500, + high: 67300, + low: 66300, + volume: 32351844, + }, + { + date: '2024-09-10', + open: 67500, + close: 67000, + high: 67800, + low: 66800, + volume: 33331844, + }, + { + date: '2024-09-09', + open: 68000, + close: 67500, + high: 68300, + low: 67300, + volume: 34311844, + }, + { + date: '2024-09-06', + open: 68500, + close: 68000, + high: 68800, + low: 67800, + volume: 35291844, + }, + { + date: '2024-09-05', + open: 69000, + close: 68500, + high: 69300, + low: 68300, + volume: 36271844, + }, + { + date: '2024-09-04', + open: 69500, + close: 69000, + high: 69800, + low: 68800, + volume: 37251844, + }, + { + date: '2024-09-03', + open: 70000, + close: 69500, + high: 70300, + low: 69300, + volume: 38231844, + }, + { + date: '2024-09-02', + open: 70500, + close: 70000, + high: 70800, + low: 69800, + volume: 39211844, + }, + { + date: '2024-08-30', + open: 71000, + close: 70500, + high: 71300, + low: 70300, + volume: 40191844, + }, + { + date: '2024-08-29', + open: 71500, + close: 71000, + high: 71800, + low: 70800, + volume: 40191844, + }, + { + date: '2024-08-28', + open: 72000, + close: 71500, + high: 72300, + low: 71300, + volume: 39171844, + }, + { + date: '2024-08-27', + open: 72500, + close: 72000, + high: 72800, + low: 71800, + volume: 38151844, + }, + { + date: '2024-08-26', + open: 73000, + close: 72500, + high: 73300, + low: 72300, + volume: 37131844, + }, + { + date: '2024-08-23', + open: 73500, + close: 73000, + high: 73800, + low: 72800, + volume: 36111844, + }, + { + date: '2024-08-22', + open: 74000, + close: 73500, + high: 74300, + low: 73300, + volume: 35091844, + }, + { + date: '2024-08-21', + open: 74500, + close: 74000, + high: 74800, + low: 73800, + volume: 34071844, + }, + { + date: '2024-08-20', + open: 75000, + close: 74500, + high: 75300, + low: 74300, + volume: 33051844, + }, + { + date: '2024-08-19', + open: 75500, + close: 75000, + high: 75800, + low: 74800, + volume: 32031844, + }, + { + date: '2024-08-16', + open: 76000, + close: 75500, + high: 76300, + low: 75300, + volume: 31011844, + }, + { + date: '2024-08-15', + open: 76500, + close: 76000, + high: 76800, + low: 75800, + volume: 29991844, + }, + { + date: '2024-08-14', + open: 77000, + close: 76500, + high: 77300, + low: 76300, + volume: 28971844, + }, + { + date: '2024-08-13', + open: 77500, + close: 77000, + high: 77800, + low: 76800, + volume: 27951844, + }, + { + date: '2024-08-12', + open: 78000, + close: 77500, + high: 78300, + low: 77300, + volume: 26931844, + }, + { + date: '2024-08-09', + open: 78500, + close: 78000, + high: 78800, + low: 77800, + volume: 25911844, + }, + { + date: '2024-08-08', + open: 79000, + close: 78500, + high: 79300, + low: 78300, + volume: 24891844, + }, + { + date: '2024-08-07', + open: 79500, + close: 79000, + high: 79800, + low: 78800, + volume: 23871844, + }, + { + date: '2024-08-06', + open: 80000, + close: 79500, + high: 80300, + low: 79300, + volume: 22851844, + }, + { + date: '2024-08-05', + open: 80500, + close: 80000, + high: 80800, + low: 79800, + volume: 21831844, + }, + { + date: '2024-08-02', + open: 81000, + close: 80500, + high: 81300, + low: 80300, + volume: 20811844, + }, + { + date: '2024-08-01', + open: 81500, + close: 81000, + high: 81800, + low: 80800, + volume: 19791844, + }, + { + date: '2024-07-31', + open: 82000, + close: 81500, + high: 82300, + low: 81300, + volume: 18771844, + }, + { + date: '2024-07-30', + open: 82500, + close: 82000, + high: 82800, + low: 81800, + volume: 17751844, + }, + { + date: '2024-07-29', + open: 83000, + close: 82500, + high: 83300, + low: 82300, + volume: 16731844, + }, + { + date: '2024-07-26', + open: 83500, + close: 83000, + high: 83800, + low: 82800, + volume: 15711844, + }, +].reverse(); From 0dddf84942c3c46a1abfdd398e59e0b41dcc6df2 Mon Sep 17 00:00:00 2001 From: dongree Date: Tue, 12 Nov 2024 17:22:52 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=8B=9C=EA=B0=80,=20?= =?UTF-8?q?=EC=A2=85=EA=B0=80=EB=A5=BC=20=EB=B0=98=EC=98=81=ED=95=9C=20?= =?UTF-8?q?=EC=BA=94=EB=93=A4=20=EC=B0=A8=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#60?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StocksDetail/Chart.tsx | 58 ++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index 337169ad..709768df 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -18,8 +18,15 @@ export default function Chart() { if (!canvas || !parent) return; - canvas.width = parent.clientWidth; - canvas.height = parent.clientHeight; + const displayWidth = parent.clientWidth; + const displayHeight = parent.clientHeight; + + // 해상도 높이기 + canvas.width = displayWidth * 4; + canvas.height = displayHeight * 4; + + canvas.style.width = `${displayWidth}px`; + canvas.style.height = `${displayHeight}px`; const ctx = canvas.getContext('2d'); if (!ctx) return; @@ -51,6 +58,8 @@ export default function Chart() { 0, chartHeight * 0.8, ); + + drawCandleChart(ctx, dummy, chartWidth, mainHeight, padding, 0, 0); }, []); return ( @@ -112,7 +121,7 @@ function drawBarChart( data.forEach((e, i) => { const value = Math.round(e.volume * 100); const cx = x + padding.left + (width * i) / (dummy.length - 1); - const cy = ((height - y) * (value - yMin)) / (yMax - yMin); + const cy = padding.top + ((height - y) * (value - yMin)) / (yMax - yMin); ctx.fillStyle = e.open < e.close ? 'red' : 'blue'; ctx.fillRect(cx, height, gap, -cy); @@ -121,3 +130,46 @@ function drawBarChart( ctx.lineWidth = 2; ctx.stroke(); } + +function drawCandleChart( + ctx: CanvasRenderingContext2D, + data: DummyStock[], + width: number, + height: number, + padding: Padding, + x: number, + y: number, +) { + ctx.beginPath(); + + const yMax = Math.round( + Math.max(...data.map((d) => Math.max(d.close, d.open))) * 1.006 * 100, + ); + const yMin = Math.round( + Math.min(...data.map((d) => Math.max(d.close, d.open))) * 0.994 * 100, + ); + + data.forEach((e, i) => { + const { open, close } = e; + const gap = Math.floor((width / dummy.length) * 0.8); + const cx = x + padding.left + (width * i) / (dummy.length - 1); + + const value1 = Math.round(e.open * 100); + const value2 = Math.round(e.close * 100); + const cy1 = + y + padding.top + height - (height * (value1 - yMin)) / (yMax - yMin); + const cy2 = + y + padding.top + height - (height * (value2 - yMin)) / (yMax - yMin); + + if (open > close) { + ctx.fillStyle = 'blue'; + ctx.fillRect(cx, cy2, gap, cy1 - cy2); + } else { + ctx.fillStyle = 'red'; + ctx.fillRect(cx, cy1, gap, cy2 - cy1); + } + }); + + ctx.lineWidth = 2; + ctx.stroke(); +} From 0e70808a5ba00acfd284e0bed28045756ac60fbe Mon Sep 17 00:00:00 2001 From: dongree Date: Tue, 12 Nov 2024 17:38:02 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=BA=94=EB=93=A4?= =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EC=B5=9C=EA=B3=A0=EA=B0=80,=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=80=EA=B0=80=20=EA=BC=AC=EB=A6=AC=EC=84=A0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20#60?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/Login/index.tsx | 10 +++--- FE/src/components/StocksDetail/Chart.tsx | 46 ++++++++++++++++-------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/FE/src/components/Login/index.tsx b/FE/src/components/Login/index.tsx index bc6f626b..eee0869e 100644 --- a/FE/src/components/Login/index.tsx +++ b/FE/src/components/Login/index.tsx @@ -46,8 +46,8 @@ export default function Login() { }[errorCode] }

-
-
+ +
- diff --git a/FE/src/components/StocksDetail/Chart.tsx b/FE/src/components/StocksDetail/Chart.tsx index 709768df..095ed44e 100644 --- a/FE/src/components/StocksDetail/Chart.tsx +++ b/FE/src/components/StocksDetail/Chart.tsx @@ -143,33 +143,51 @@ function drawCandleChart( ctx.beginPath(); const yMax = Math.round( - Math.max(...data.map((d) => Math.max(d.close, d.open))) * 1.006 * 100, + Math.max(...data.map((d) => Math.max(d.close, d.open, d.high, d.low))) * + 1.006 * + 100, ); const yMin = Math.round( - Math.min(...data.map((d) => Math.max(d.close, d.open))) * 0.994 * 100, + Math.min(...data.map((d) => Math.max(d.close, d.open, d.high, d.low))) * + 0.994 * + 100, ); data.forEach((e, i) => { - const { open, close } = e; + ctx.beginPath(); + + const { open, close, high, low } = e; const gap = Math.floor((width / dummy.length) * 0.8); const cx = x + padding.left + (width * i) / (dummy.length - 1); - const value1 = Math.round(e.open * 100); - const value2 = Math.round(e.close * 100); - const cy1 = - y + padding.top + height - (height * (value1 - yMin)) / (yMax - yMin); - const cy2 = - y + padding.top + height - (height * (value2 - yMin)) / (yMax - yMin); + const openValue = Math.round(open * 100); + const closeValue = Math.round(close * 100); + const highValue = Math.round(high * 100); + const lowValue = Math.round(low * 100); + + const openY = + y + padding.top + height - (height * (openValue - yMin)) / (yMax - yMin); + const closeY = + y + padding.top + height - (height * (closeValue - yMin)) / (yMax - yMin); + const highY = + y + padding.top + height - (height * (highValue - yMin)) / (yMax - yMin); + const lowY = + y + padding.top + height - (height * (lowValue - yMin)) / (yMax - yMin); if (open > close) { ctx.fillStyle = 'blue'; - ctx.fillRect(cx, cy2, gap, cy1 - cy2); + ctx.strokeStyle = 'blue'; + ctx.fillRect(cx, closeY, gap, openY - closeY); } else { ctx.fillStyle = 'red'; - ctx.fillRect(cx, cy1, gap, cy2 - cy1); + ctx.strokeStyle = 'red'; + ctx.fillRect(cx, openY, gap, closeY - openY); } - }); - ctx.lineWidth = 2; - ctx.stroke(); + const middle = cx + Math.floor(gap / 2); + + ctx.moveTo(middle, highY); + ctx.lineTo(middle, lowY); + ctx.stroke(); + }); } From 3462d8bf50289690180dbb2b50258f64b9bebab2 Mon Sep 17 00:00:00 2001 From: dongree Date: Tue, 12 Nov 2024 17:52:54 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=92=84=20design:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20detail=20=ED=8E=98=EC=9D=B4=EC=A7=80=20header=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=ED=98=84=20#60?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/StocksDetail/Header.tsx | 37 +++++++++++++++++++++-- FE/src/page/StocksDetail.tsx | 2 +- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/FE/src/components/StocksDetail/Header.tsx b/FE/src/components/StocksDetail/Header.tsx index 91309470..767746c8 100644 --- a/FE/src/components/StocksDetail/Header.tsx +++ b/FE/src/components/StocksDetail/Header.tsx @@ -1,8 +1,39 @@ export default function Header() { return ( -
-
삼성전자
-
당기순이익
+
+
+
+

삼성전자

+

005930

+
+
+

60,900원

+

어제보다

+

+1800원 (3.0%)

+
+
+
+
+

당기순이익

+

9조 8,143억

+
+
+

영업이익

+

10조 4,439억

+
+
+

매출액

+

74조 683억

+
+
+

시총

+

361조 1,718억

+
+
+

PER

+

14.79배

+
+
); } diff --git a/FE/src/page/StocksDetail.tsx b/FE/src/page/StocksDetail.tsx index 536ef091..2eee8274 100644 --- a/FE/src/page/StocksDetail.tsx +++ b/FE/src/page/StocksDetail.tsx @@ -5,7 +5,7 @@ import TradeSection from 'components/StocksDetail/TradeSection'; export default function StocksDetail() { return ( -
+