diff --git a/client/package-lock.json b/client/package-lock.json index 5a82f06..8172c99 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -25,7 +25,8 @@ "react-spinners": "^0.13.8", "react-toastify": "^9.1.3", "redux-persist": "^6.0.0", - "styled-components": "^6.0.7" + "styled-components": "^6.0.7", + "webstomp-client": "^1.2.6" }, "devDependencies": { "@types/node": "^20.6.0", @@ -5503,6 +5504,11 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webstomp-client": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/webstomp-client/-/webstomp-client-1.2.6.tgz", + "integrity": "sha512-9HajO6Ki2ViEGIusLZtjM2lcO2VaQUvtXhLQQ4Cm543RLjfTCEgI3sFaiXts3TvfZgrtY/vI/+qUkm2qWD/NVg==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/client/package.json b/client/package.json index 6db709e..522e624 100644 --- a/client/package.json +++ b/client/package.json @@ -27,7 +27,8 @@ "react-spinners": "^0.13.8", "react-toastify": "^9.1.3", "redux-persist": "^6.0.0", - "styled-components": "^6.0.7" + "styled-components": "^6.0.7", + "webstomp-client": "^1.2.6" }, "devDependencies": { "@types/node": "^20.6.0", diff --git a/client/src/components/CentralChartMenu/ExpandScreenBtn.tsx b/client/src/components/CentralChartMenu/ExpandScreenBtn.tsx index f322c54..971a9d1 100644 --- a/client/src/components/CentralChartMenu/ExpandScreenBtn.tsx +++ b/client/src/components/CentralChartMenu/ExpandScreenBtn.tsx @@ -69,7 +69,8 @@ interface OwnProps { // component 생성 const Button = styled.div` - width: ${(props) => (props.buttonHover ? "46px" : "8px")}; + width: ${(props) => (props.buttonHover ? "46px" : "6px")}; + /* width: 46px; */ height: 100%; display: flex; justify-content: center; diff --git a/client/src/components/CentralChartMenu/Index.tsx b/client/src/components/CentralChartMenu/Index.tsx index 90e06f8..2462078 100644 --- a/client/src/components/CentralChartMenu/Index.tsx +++ b/client/src/components/CentralChartMenu/Index.tsx @@ -1,23 +1,32 @@ +import { useSelector } from "react-redux"; import { styled } from "styled-components"; +import useGetStockInfo from "../../hooks/useGetStockInfo"; +import { StateProps } from "../../models/stateProps"; import ExpandScreenBtn from "./ExpandScreenBtn"; import StockOverview from "./StockOverview"; import StockOrderBtn from "./StockOrderBtn"; -// import CompareChartBtn from "./CompareChartBtn"; const UpperMenuBar = () => { + const companyId = useSelector((state: StateProps) => state.companyId); + const { stockInfoLoading, stockInfoError } = useGetStockInfo(companyId); + + if (stockInfoLoading) { + return <>; + } + + if (stockInfoError) { + return

에러 발생

; + } + return (
- {/* */}
- {/*
- -
*/}
); }; diff --git a/client/src/components/CentralChartMenu/StockOverview.tsx b/client/src/components/CentralChartMenu/StockOverview.tsx index 15f0123..267b881 100644 --- a/client/src/components/CentralChartMenu/StockOverview.tsx +++ b/client/src/components/CentralChartMenu/StockOverview.tsx @@ -27,7 +27,7 @@ const valueText: string = "거래대금"; // styled-component 수정 및 미디어 쿼리 적용 const StockOverview = () => { const companyId = useSelector((state: StateProps) => state.companyId); - const { stockInfo, stockInfoLoading, stockInfoError } = useGetStockInfo(companyId); + const { stockInfo } = useGetStockInfo(companyId); const corpName = stockInfo?.korName; @@ -54,13 +54,13 @@ const StockOverview = () => { return null; // 혹은 다른 적절한 렌더링을 반환 } - if (stockInfoLoading) { - return

로딩 중 입니다

; - } + // if (stockInfoLoading) { + // return <>; + // } - if (stockInfoError) { - return

에러 발생

; - } + // if (stockInfoError) { + // return

에러 발생

; + // } const stockCode = stockInfo.code; const stockPrice = parseInt(stockInfo.stockInfResponseDto.stck_prpr, 10).toLocaleString(); diff --git a/client/src/components/StockOrderSection/Index.tsx b/client/src/components/StockOrderSection/Index.tsx index e6039f6..eb42854 100644 --- a/client/src/components/StockOrderSection/Index.tsx +++ b/client/src/components/StockOrderSection/Index.tsx @@ -59,7 +59,7 @@ const StockOrderSection: React.FC = (props) => { // 1) 데이터 로딩 중 if (isLoading) { - return 로딩 중; + return ; } // 주식주문 창 닫기 @@ -149,7 +149,7 @@ const StockOrderSection: React.FC = (props) => { ) : ( - //props전달 + //props전달 )} ); @@ -159,17 +159,18 @@ export default StockOrderSection; interface StockOrderSectionProps { openOAuthModal: () => void; - openProfileModal: () => void; // Add this line + openProfileModal: () => void; // Add this line } - // 미로그인 시 -> 로그인 요청 화면 //props 전달 const LoginRequestIndicator: React.FC = ({ openOAuthModal }) => { return (
{loginRequiredText}
- +
); }; @@ -183,7 +184,9 @@ const MoneyReqireIndicator: React.FC = ({ openProfile return (
{moneyRequireText}
- +
); }; @@ -285,7 +288,7 @@ const LoginRequestContainer = styled.div` background-color: #2f4f4f; border: none; border-radius: 0.3rem; - cursor: pointer; + cursor: pointer; } `; diff --git a/client/src/components/StockOrderSection/StockOrder.tsx b/client/src/components/StockOrderSection/StockOrder.tsx index 25de2df..f6012e3 100644 --- a/client/src/components/StockOrderSection/StockOrder.tsx +++ b/client/src/components/StockOrderSection/StockOrder.tsx @@ -1,5 +1,5 @@ import { useSelector, useDispatch } from "react-redux"; -// import { isHoliday } from "@hyunbinseo/holidays-kr"; +import { isHoliday } from "@hyunbinseo/holidays-kr"; import { setStockOrderVolume } from "../../reducer/StockOrderVolume-Reducer"; import { closeDecisionWindow } from "../../reducer/SetDecisionWindow-Reducer"; import { styled } from "styled-components"; @@ -123,22 +123,22 @@ const StockOrder = ({ corpName }: { corpName: string }) => { }; // 1) 주말, 공휴일 여부 체크 - // const today = new Date(); - // const nonBusinessDay = isHoliday(today, { include: { saturday: true, sunday: true } }); // 토요일, 일요일, 공휴일 (임시 공휴일 포함) + const today = new Date(); + const nonBusinessDay = isHoliday(today, { include: { saturday: true, sunday: true } }); // 토요일, 일요일, 공휴일 (임시 공휴일 포함) // 🟢 2) 개장시간 여부 체크 - // const currentHour = today.getHours(); - // const currentMinute = today.getMinutes(); - // const isBefore9AM = currentHour < 9; - // const isAfter330PM = currentHour > 15 || (currentHour === 15 && currentMinute >= 30); - // const closingTime = isBefore9AM || isAfter330PM; + const currentHour = today.getHours(); + const currentMinute = today.getMinutes(); + const isBefore9AM = currentHour < 9; + const isAfter330PM = currentHour > 15 || (currentHour === 15 && currentMinute >= 30); + const closingTime = isBefore9AM || isAfter330PM; // 주문 실패 케이스 1) 개장시간 2) 가격/거래량 설정 // 🔴 3시 30분 이후 작업 위해 closingTime 조건 해제 + 주말 요건도 해제 - const orderFailureCase01 = false; + // const orderFailureCase01 = false; // 🟢 기존로직 - // const orderFailureCase01 = nonBusinessDay || closingTime; + const orderFailureCase01 = nonBusinessDay || closingTime; const orderFailureCase02 = orderPrice === 0 || orderVolume === 0; return ( diff --git a/client/src/components/StockOrderSection/WaitOrderIndicator.tsx b/client/src/components/StockOrderSection/WaitOrderIndicator.tsx index 1450280..67f0379 100644 --- a/client/src/components/StockOrderSection/WaitOrderIndicator.tsx +++ b/client/src/components/StockOrderSection/WaitOrderIndicator.tsx @@ -1,31 +1,131 @@ -import { useEffect } from "react"; +import { styled } from "styled-components"; import { toast } from "react-toastify"; +import { useState, useEffect } from "react"; +import useGetMemberId from "../../hooks/useGetMemberId"; +import * as Webstomp from "webstomp-client"; -import useGetWaitOrderSuccessInfo from "../../hooks/useGetWaitOrderSuccessInfo"; +const url = "ws://ec2-13-125-246-160.ap-northeast-2.compute.amazonaws.com:8080/ws"; -const WaitOrderIndicator = () => { - const { waitOrderSuccessData } = useGetWaitOrderSuccessInfo(); +//기업로고 import +import kia from "../../asset/logos/기아.svg"; +import dy from "../../asset/logos/디와이.jpeg"; +import logosamsung from "../../asset/logos/삼성전자.svg"; +import celltrion from "../../asset/logos/셀트리온.svg"; +import ecopro from "../../asset/logos/에코프로.jpeg"; +import ecoproBM from "../../asset/logos/에코프로비엠.svg"; +import kakaoBank from "../../asset/logos/카카오뱅크.svg"; +import kuckoo from "../../asset/logos/쿠쿠홀딩스.jpeg"; +import hanse from "../../asset/logos/한세엠케이.jpeg"; +import hyundai from "../../asset/logos/현대차.svg"; +import KG from "../../asset/logos/KG케미칼.png"; +import LGelec from "../../asset/logos/LG전자.svg"; +import LGchem from "../../asset/logos/LG화학.svg"; +import posco from "../../asset/logos/POSCO홀딩스.svg"; + +const corpLogo = [logosamsung, posco, celltrion, ecopro, ecoproBM, dy, kuckoo, kakaoBank, hanse, KG, LGchem, hyundai, LGelec, kia]; +const corpName = ["삼성전자", "POSCO홀딩스", "셀트리온", "에코프로", "에코프로비엠", "디와이", "쿠쿠홀딩스", "카카오뱅크", "한세엠케이", "KG케마칼", "LG화학", "현대차", "LG전자", "기아"]; - const toastStyle = { - fontSize: "15px", - fontWeight: 450, - color: "#2679ed", - }; +const volumeUnit = "주"; +const toastText01: string = "미체결"; +const toastText02: string = " 체결 처리 되었습니다"; + +const WaitOrderIndicator = () => { + const { memberId } = useGetMemberId(); + const [waitOrder, setWaitOrder] = useState(null); useEffect(() => { - if (waitOrderSuccessData) { - console.log(waitOrderSuccessData); + if (memberId) { + const socket = new WebSocket(url); + const stompClient = Webstomp.over(socket); - if (waitOrderSuccessData[0].length !== 0 && waitOrderSuccessData[1].length !== 0) { - toast.info(`대기주문이 체결되었습니다`, { - style: toastStyle, - position: "bottom-left", + const headers = { + memberId: memberId, + }; + + stompClient.connect(headers, () => { + console.log("Connected with headers:", headers); + console.log("Connected to the WebSocket server"); + + stompClient.subscribe(`/sub/${memberId}`, async (data) => { + const responseData = JSON.parse(data.body); + const orderType = responseData.orderTypes === "BUY" ? "매수" : "매도"; + const orderVolume = responseData.stockCount; + const companyId = responseData.companyId; + const result = { companyId: companyId, orderType: orderType, orderVolume: orderVolume }; + setWaitOrder(result); }); - } + }); + } + }, [memberId]); + + useEffect(() => { + if (waitOrder !== null) { + const companyId = waitOrder.companyId; + const type = waitOrder.orderType; + const logo = corpLogo[companyId - 1]; + const name = corpName[companyId - 1]; + const volume = waitOrder.orderVolume; + + toast( + +
+ stock logo +
+ {name} {volume} + {volumeUnit} +
+
+
+ + ✓ {toastText01} {type} + + {toastText02} +
+
, + { + position: toast.POSITION.BOTTOM_LEFT, + // autoClose: 2000, + hideProgressBar: true, + } + ); } - }, [waitOrderSuccessData]); + }, [waitOrder]); return
; }; export default WaitOrderIndicator; + +interface WaitOrderProps { + companyId: number; + orderType: string; + orderVolume: number; +} + +const ToastMessage = styled.div<{ orderType: string }>` + display: flex; + flex-direction: column; + gap: 7px; + + font-size: 14px; + + .overview { + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + font-weight: 700; + gap: 6px; + } + + & img { + width: 24px; + height: 24px; + border-radius: 50%; + padding-bottom: 3px; + } + + .orderType { + color: ${(props) => (props.orderType === "매수" ? "#e22926" : "#2679ed")}; + } +`; diff --git a/client/src/components/communityComponents/index.tsx b/client/src/components/communityComponents/index.tsx index e18e3d8..6cd122f 100644 --- a/client/src/components/communityComponents/index.tsx +++ b/client/src/components/communityComponents/index.tsx @@ -154,7 +154,7 @@ const TimeLineComponent = () => { Submit - Cancle + Cancel )} diff --git a/client/src/hooks/useGetMemberId.ts b/client/src/hooks/useGetMemberId.ts new file mode 100644 index 0000000..2a3ca76 --- /dev/null +++ b/client/src/hooks/useGetMemberId.ts @@ -0,0 +1,31 @@ +import { useSelector } from "react-redux"; +import { useQuery } from "react-query"; +import { StateProps } from "../models/stateProps"; +import axios from "axios"; + +const url = "http://ec2-13-125-246-160.ap-northeast-2.compute.amazonaws.com:8080/members"; + +const useGetMemberId = () => { + const login = useSelector((state: StateProps) => state.login); + const { data, isLoading, isError } = useQuery("memberId", getMemberId, { + enabled: login === 1, + }); + + return { memberId: data, memberIdLoading: isLoading, memberIdError: isError }; +}; + +export default useGetMemberId; + +const getMemberId = async () => { + const accessToken = localStorage.getItem("accessToken"); + const options = { + headers: { + Authorization: `${accessToken}`, + "Content-Type": "application/json", + }, + }; + + const respone = await axios.get(url, options); + const memberId = await respone.data.memberId; + return memberId; +}; diff --git a/client/src/hooks/useGetStockData.ts b/client/src/hooks/useGetStockData.ts index ebf0332..5b15809 100644 --- a/client/src/hooks/useGetStockData.ts +++ b/client/src/hooks/useGetStockData.ts @@ -1,4 +1,4 @@ -// 🟢 기존 로직 +import { isHoliday } from "@hyunbinseo/holidays-kr"; import { useState, useEffect } from "react"; import { useQuery, useQueryClient } from "react-query"; import axios from "axios"; @@ -14,49 +14,50 @@ const useGetStockData = (companyId: number) => { const timeZone = minute === 0 || minute === 30 ? "30 or 60" : 0 < minute && minute < 30 ? "1~29" : "31~59"; const queryKey = `${month}월 ${day}일 ${hour}시 ${timeZone}`; - // 현재 시각이 30분, 정각이 아닌 경우 남은 시간 계산하여 checkTime 함수 다시 실행 + // 1) 주말, 공휴일 여부 체크 + const today = new Date(); + const nonBusinessDay = isHoliday(today, { include: { saturday: true, sunday: true } }); // 토요일, 일요일, 공휴일 (임시 공휴일 포함) + + // 🟢 2) 개장시간 여부 체크 + const currentHour = today.getHours(); + const currentMinute = today.getMinutes(); + const isBefore9AM = currentHour < 9; + const isAfter330PM = currentHour > 15 || (currentHour === 15 && currentMinute >= 30); + const closingTime = isBefore9AM || isAfter330PM; + + // 🟢 기존로직 + const notRenwalTime = nonBusinessDay || closingTime; + + // 개장 시간 이내일 경우, 현재 시각이 30분, 정각이 아닌 경우 남은 시간 계산하여 checkTime 함수 다시 실행 useEffect(() => { - if (minute === 0 || minute === 30) { - setAutoRefetch(true); - } else if (0 < minute && minute < 30) { - const delayTime = (30 - minute) * 60000; - setTimeout(() => { - refetch(); - setAutoRefetch(true); - }, delayTime); - } else if (30 < minute && minute < 60) { - const delayTime = (60 - minute) * 60000; - setTimeout(() => { - refetch(); + if (!notRenwalTime) { + if (minute === 0 || minute === 30) { setAutoRefetch(true); - }, delayTime); + } else if (0 < minute && minute < 30) { + const delayTime = (30 - minute) * 60000; + setTimeout(() => { + refetch(); + setAutoRefetch(true); + }, delayTime); + } else if (30 < minute && minute < 60) { + const delayTime = (60 - minute) * 60000; + setTimeout(() => { + refetch(); + setAutoRefetch(true); + }, delayTime); + } } }, []); const { data, isLoading, error, refetch } = useQuery(`chartData${companyId} ${queryKey}`, () => getChartData(companyId), { enabled: true, - refetchInterval: autoRefetch ? 60000 * 10 : false, // 정각 혹은 30분에 맞춰서 10분 마다 데이터 리패칭 + refetchInterval: autoRefetch && !notRenwalTime ? 60000 * 30 : false, // 정각 혹은 30분에 맞춰서 30분 마다 데이터 리패칭 onSuccess: () => { queryClient.invalidateQueries("cash"); queryClient.invalidateQueries("holdingStock"); queryClient.invalidateQueries("orderRecord"); }, }); - // 🟢 기존 로직 - - // 🔴 테스트 로직 - // const queryClient = useQueryClient(); - - // const { data, isLoading, error } = useQuery(`chartData`, () => getChartData(companyId), { - // enabled: true, - // refetchInterval: 1000 * 10, // 정각 혹은 30분에 맞춰서 10분 마다 데이터 리패칭 - // onSuccess: () => { - // queryClient.invalidateQueries("cash"); - // queryClient.invalidateQueries("holdingStock"); - // queryClient.invalidateQueries("orderRecord"); - // }, - // }); - // 🔴 테스트 로직 return { stockPrice: data, stockPriceLoading: isLoading, stockPriceError: error }; }; diff --git a/client/src/hooks/useGetStockInfo.ts b/client/src/hooks/useGetStockInfo.ts index 3460774..9a38d77 100644 --- a/client/src/hooks/useGetStockInfo.ts +++ b/client/src/hooks/useGetStockInfo.ts @@ -1,3 +1,4 @@ +import { isHoliday } from "@hyunbinseo/holidays-kr"; import { useState, useEffect } from "react"; import { useQuery } from "react-query"; import axios from "axios"; @@ -11,29 +12,44 @@ const useGetStockInfo = (companyId: number) => { const timeZone = minute === 0 || minute === 30 ? "30 or 60" : 0 < minute && minute < 30 ? "1~29" : "31~59"; const queryKey = `${month}월 ${day}일 ${hour}시 ${timeZone}`; - // 현재 시각이 30분, 정각이 아닌 경우 남은 시간 계산하여 checkTime 함수 다시 실행 + // 1) 주말, 공휴일 여부 체크 + const today = new Date(); + const nonBusinessDay = isHoliday(today, { include: { saturday: true, sunday: true } }); // 토요일, 일요일, 공휴일 (임시 공휴일 포함) + + // 🟢 2) 개장시간 여부 체크 + const currentHour = today.getHours(); + const currentMinute = today.getMinutes(); + const isBefore9AM = currentHour < 9; + const isAfter330PM = currentHour > 15 || (currentHour === 15 && currentMinute >= 30); + const closingTime = isBefore9AM || isAfter330PM; + + // 🟢 기존로직 + const notRenwalTime = nonBusinessDay || closingTime; + + // 개장 시간 이내일 경우, 현재 시각이 30분, 정각이 아닌 경우 남은 시간 계산하여 checkTime 함수 다시 실행 useEffect(() => { - if (minute === 0 || minute === 30) { - setAutoRefetch(true); - } else if (0 < minute && minute < 30) { - const delayTime = (30 - minute) * 60000; - setTimeout(() => { - refetch(); + if (!notRenwalTime) { + if (minute === 0 || minute === 30) { setAutoRefetch(true); - }, delayTime); - } else if (30 < minute && minute < 60) { - const delayTime = (60 - minute) * 60000; - setTimeout(() => { - refetch(); - setAutoRefetch(true); - }, delayTime); + } else if (0 < minute && minute < 30) { + const delayTime = (30 - minute) * 60000; + setTimeout(() => { + refetch(); + setAutoRefetch(true); + }, delayTime); + } else if (30 < minute && minute < 60) { + const delayTime = (60 - minute) * 60000; + setTimeout(() => { + refetch(); + setAutoRefetch(true); + }, delayTime); + } } }, []); const { data, isLoading, error, refetch } = useQuery(`stockInfo${companyId} ${queryKey}}`, () => getStockInfo(companyId), { enabled: true, - refetchInterval: autoRefetch ? 60000 * 10 : false, // 정각 혹은 30분에 맞춰서 10분 마다 데이터 리패칭 - + refetchInterval: autoRefetch && !notRenwalTime ? 60000 * 30 : false, // 정각 혹은 30분에 맞춰서 30분 마다 데이터 리패칭 }); return { stockInfo: data, stockInfoLoading: isLoading, stockInfoError: error };