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>
 );