From 8f6a761b1889eb2f74cf2d1375234f9d5c52fe49 Mon Sep 17 00:00:00 2001 From: novice1993 <novice1993@gmail.com> Date: Tue, 5 Sep 2023 04:08:39 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EC=B0=A8=ED=8A=B8=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=20=EB=B0=8F=20=EA=B7=B8=EB=9E=98=ED=94=BC=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버에서 데이터 패칭 후 차트 그리는 기본 로직 구현 - 최초 렌더링 이후 정각 혹은 30분 마다 API 호출하여 데이터 갱신되도록 로직 구현 - 현재 단일 주식 로직만 구현된 상황으로, 추후 로직 확장 예정 Issues #14 --- client/package-lock.json | 101 +++++++++++++++++- client/package.json | 1 + .../components/CentralChart/StockChart.tsx | 83 ++++---------- client/src/hooks/README.md | 0 client/src/hooks/useGetChart.ts | 73 +++++++++++++ client/src/hooks/useGetStockData.ts | 47 ++++++++ client/src/main.tsx | 11 +- 7 files changed, 247 insertions(+), 69 deletions(-) delete mode 100644 client/src/hooks/README.md create mode 100644 client/src/hooks/useGetChart.ts create mode 100644 client/src/hooks/useGetStockData.ts diff --git a/client/package-lock.json b/client/package-lock.json index 45f96031..0c0f951b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,6 +14,7 @@ "echarts-for-react": "^3.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-redux": "^8.1.2", "react-router-dom": "^6.15.0", "styled-components": "^6.0.7" @@ -2945,6 +2946,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2975,6 +2984,21 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/browserslist": { "version": "4.21.10", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", @@ -3223,6 +3247,11 @@ "node": ">=0.4.0" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4006,6 +4035,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4130,6 +4164,15 @@ "yallist": "^3.0.2" } }, + "node_modules/match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4152,6 +4195,11 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4187,6 +4235,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -4224,6 +4280,11 @@ "node": ">=0.10.0" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4459,6 +4520,31 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-redux": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", @@ -4628,6 +4714,11 @@ "jsesc": "bin/jsesc" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "node_modules/reselect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", @@ -4672,7 +4763,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -5052,6 +5142,15 @@ "node": ">=4" } }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", diff --git a/client/package.json b/client/package.json index a313d8f4..6012c6ce 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,7 @@ "echarts-for-react": "^3.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-redux": "^8.1.2", "react-router-dom": "^6.15.0", "styled-components": "^6.0.7" diff --git a/client/src/components/CentralChart/StockChart.tsx b/client/src/components/CentralChart/StockChart.tsx index 4d6ed036..09f5c80f 100644 --- a/client/src/components/CentralChart/StockChart.tsx +++ b/client/src/components/CentralChart/StockChart.tsx @@ -1,7 +1,25 @@ +import { useEffect } from "react"; import { styled } from "styled-components"; import EChartsReact from "echarts-for-react"; +import useGetStockData from "../../hooks/useGetStockData"; +import useGetChart from "../../hooks/useGetChart"; const StockChart = () => { + const { data, isLoading, error } = useGetStockData(); + const { options, chartStyle } = useGetChart(); + + useEffect(() => { + console.log(data); + }, [data]); + + if (isLoading) { + return <p>Loading</p>; + } + + if (error) { + return <p>error</p>; + } + return ( <Container> <EChartsReact option={options} style={chartStyle} /> @@ -11,71 +29,6 @@ const StockChart = () => { export default StockChart; -const options = { - xAxis: { - type: "category", - }, - yAxis: [ - { - type: "value", - position: "right", // 오른쪽에 위치 - }, - ], - dataZoom: [ - { - type: "inside", // 마우스 스크롤을 통한 줌 인/아웃 지원 - }, - ], - tooltip: { - trigger: "axis", - axisPointer: { - type: "cross", // 마우스 위치에 눈금 표시 - }, - }, - series: [ - { - name: "주가", - type: "candlestick", // 캔들스틱 시리즈 - data: [ - [100, 120, 80, 90], // 시가, 종가, 저가, 주가 - [110, 130, 100, 120], - [90, 110, 70, 100], - [95, 105, 85, 110], - [105, 125, 95, 120], - [110, 120, 100, 130], - [120, 140, 110, 150], - [130, 150, 120, 160], - [140, 160, 130, 170], - [150, 170, 140, 180], - [150, 170, 140, 180], - [160, 180, 150, 190], - [170, 190, 160, 200], - [170, 200, 170, 210], - [170, 140, 130, 130], - [150, 160, 120, 160], - [140, 160, 130, 170], - [150, 170, 140, 180], - [140, 125, 95, 120], - [110, 120, 100, 130], - [120, 140, 110, 150], - [130, 150, 120, 160], - [140, 160, 130, 170], - [150, 170, 140, 180], - [160, 180, 150, 190], - [170, 190, 160, 200], - [180, 200, 170, 210], - [190, 210, 180, 220], - ], - yAxisIndex: 0, // 첫 번째 Y 축 사용 - }, - ], -}; - -const chartStyle = { - width: "100%", - height: "100%", -}; - const Container = styled.div` height: 100%; display: flex; diff --git a/client/src/hooks/README.md b/client/src/hooks/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/hooks/useGetChart.ts b/client/src/hooks/useGetChart.ts new file mode 100644 index 00000000..f8d3fa8e --- /dev/null +++ b/client/src/hooks/useGetChart.ts @@ -0,0 +1,73 @@ +import { useState, useEffect } from "react"; +import useGetStockData from "./useGetStockData"; + +const useGetChart = () => { + const { data } = useGetStockData(); + const [chartData, setChartData] = useState([]); + + useEffect(() => { + if (data) { + setChartData(data); + } + }, [data]); + + const options = { + xAxis: { + type: "category", + data: chartData.map((stock: StockProps) => { + const date = new Date(stock.stockTradeTime); + const tradeTime = `${date.getHours()}:${date.getMinutes()}`; + return tradeTime; + }), + }, + yAxis: [ + { + type: "value", + position: "right", + interval: 100, + min: 70000, + }, + ], + dataZoom: [ + { + type: "inside", + }, + ], + tooltip: { + trigger: "axis", + axisPointer: { + type: "cross", + }, + }, + series: [ + { + name: "주가", + type: "candlestick", + data: chartData.map((stock: StockProps) => { + return [stock.stck_oprc, stock.stck_prpr, stock.stck_lwpr, stock.stck_hgpr]; + }), + yAxisIndex: 0, + }, + ], + }; + + const chartStyle = { + width: "100%", + height: "100%", + }; + + return { options, chartStyle }; +}; + +export default useGetChart; + +interface StockProps { + stockMinId: number; + companyId: number; + stockTradeTime: string; + stck_cntg_hour: string; + stck_prpr: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; +} diff --git a/client/src/hooks/useGetStockData.ts b/client/src/hooks/useGetStockData.ts new file mode 100644 index 00000000..a045866c --- /dev/null +++ b/client/src/hooks/useGetStockData.ts @@ -0,0 +1,47 @@ +import { useState, useEffect } from "react"; +import { useQuery } from "react-query"; +import axios from "axios"; + +const useGetStockData = () => { + const [fetching, setFetching] = useState(true); + + // 30분 or 정각여부 체크 함수 + const checkTime = () => { + const currentTime = new Date(); + const minute = currentTime.getMinutes(); + + (minute === 0 || minute === 30) && setFetching(false); + return minute; + }; + + // 현재 시각이 30분, 정각이 아닌 경우 남은 시간 계산하여 checkTime 함수 다시 실행 + useEffect(() => { + const checkMinute = checkTime(); + + if (0 < checkMinute && checkMinute < 30) { + const delayTime = (30 - checkMinute) * 60000; + setTimeout(checkTime, delayTime); + } + if (30 < checkMinute && checkMinute < 60) { + const delayTime = (60 - checkMinute) * 60000; + setTimeout(checkTime, delayTime); + } + }, []); + + // 30분 정각이 될경우 서버 데이터 호출 + 30분 마다 데이터 갱신 + const { data, isLoading, error } = useQuery("chartData", getChartData, { + enabled: fetching, + refetchInterval: 60000 * 30, + refetchOnMount: true, + }); + + return { data, isLoading, error }; +}; + +export default useGetStockData; + +// 차트 데이터 받아오는 fetching 로직 +const getChartData = async () => { + const res = await axios.get("http://ec2-13-125-246-160.ap-northeast-2.compute.amazonaws.com/companies/charts/1"); + return res.data; +}; diff --git a/client/src/main.tsx b/client/src/main.tsx index dc075919..3d19ea5c 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,13 +1,18 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; +import { QueryClientProvider, QueryClient } from "react-query"; import { Provider } from "react-redux"; import store from "./store/config.ts"; +const queryClient = new QueryClient(); + ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> - <Provider store={store}> - <App /> - </Provider> + <QueryClientProvider client={queryClient}> + <Provider store={store}> + <App /> + </Provider> + </QueryClientProvider> </React.StrictMode> );