diff --git a/client/src/components/CentralChartMenu/StockOverview.tsx b/client/src/components/CentralChartMenu/StockOverview.tsx index e8687a86..3e1f2d49 100644 --- a/client/src/components/CentralChartMenu/StockOverview.tsx +++ b/client/src/components/CentralChartMenu/StockOverview.tsx @@ -99,14 +99,14 @@ const Container = styled.div<{ priceChangeRate: number }>` .StockPrice { font-size: 18px; - color: ${(props) => (props.priceChangeRate > 0 ? "#ed2926" : "#3177d7")}; + color: ${(props) => (props.priceChangeRate > 0 ? "#ed2926" : props.priceChangeRate === 0 ? "black" : "#3177d7")}; font-weight: 530; } .PriceChangeRate, .PriceChangeAmount { font-size: 14px; - color: ${(props) => (props.priceChangeRate > 0 ? "#ed2926" : "#3177d7")}; + color: ${(props) => (props.priceChangeRate > 0 ? "#ed2926" : props.priceChangeRate === 0 ? "black" : "#3177d7")}; display: flex; flex-direction: row; diff --git a/client/src/components/StockOrderSection/Index.tsx b/client/src/components/StockOrderSection/Index.tsx index a23249e0..593545c1 100644 --- a/client/src/components/StockOrderSection/Index.tsx +++ b/client/src/components/StockOrderSection/Index.tsx @@ -70,7 +70,8 @@ const Container = styled.aside<{ orderSet: boolean }>` width: 26%; min-width: 400px; height: 100%; - border-left: 1px solid black; + box-shadow: -1px 0px 10px darkgray; + background-color: #ffffff; `; @@ -81,8 +82,8 @@ const UpperBar = styled.div` justify-content: center; align-items: center; width: 100%; - min-height: 43px; - border-bottom: 1px solid black; + min-height: 44px; + border-bottom: 1px solid darkgray; .Title { font-size: 17px; @@ -112,7 +113,6 @@ const StockName = styled.section` padding-bottom: 8px; padding-left: 16px; gap: 9px; - border-bottom: 1px solid black; .CorpLogo { width: 28px; diff --git a/client/src/components/StockOrderSection/OrderRequest.tsx b/client/src/components/StockOrderSection/OrderRequest.tsx index 11563d04..6c71f33a 100644 --- a/client/src/components/StockOrderSection/OrderRequest.tsx +++ b/client/src/components/StockOrderSection/OrderRequest.tsx @@ -16,7 +16,6 @@ export default OrderRequest; const Container = styled.div` height: 414px; - border-bottom: 1px solid black; display: flex; flex-direction: row; diff --git a/client/src/components/StockOrderSection/StockOrderBtn.tsx b/client/src/components/StockOrderSection/StockOrderBtn.tsx index 66389801..45c569b3 100644 --- a/client/src/components/StockOrderSection/StockOrderBtn.tsx +++ b/client/src/components/StockOrderSection/StockOrderBtn.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; import { styled } from "styled-components"; import { StateProps } from "../../models/stateProps"; @@ -5,18 +6,30 @@ import { OrderTypeProps } from "../../models/orderTypeProps"; const availableMoneyText01: string = "최대"; const availableMoneyText02: string = "원"; -const totalAmountText01: string = "주문총액"; -const totalAmountText02: string = "원"; +const totalAmountText: string = "주문총액"; +const totalAmountUnit: string = "원"; // dummyData import { availableMoney } from "./dummyData"; -const dummyAmount: string = "0"; const dummyMoney = availableMoney.toLocaleString(); -const StockOrderBtn = () => { +const StockOrderBtn = (props: OwnProps) => { + const { orderVolume, setOrderVolume } = props; + const stockOrderType = useSelector((state: StateProps) => state.stockOrderType); + const orderPrice = useSelector((state: StateProps) => state.stockOrderPrice); + const [totalOrderAmout, setTotalOrderAmout] = useState(0); const orderBtnText: string = stockOrderType ? "매도" : "매수"; + useEffect(() => { + setTotalOrderAmout(orderPrice * orderVolume); + }, [orderPrice, orderVolume]); + + useEffect(() => { + setOrderVolume(0); + setTotalOrderAmout(0); + }, [stockOrderType]); + return ( @@ -25,9 +38,9 @@ const StockOrderBtn = () => { {availableMoneyText02} - {totalAmountText01} - {dummyAmount} - {totalAmountText02} + {totalAmountText} + {totalOrderAmout.toLocaleString()} + {totalAmountUnit} {orderBtnText} @@ -36,6 +49,11 @@ const StockOrderBtn = () => { export default StockOrderBtn; +interface OwnProps { + orderVolume: number; + setOrderVolume: (orderVolume: number) => void; +} + const Container = styled.div``; const AvailableMoney = styled.div<{ orderType: boolean }>` @@ -67,7 +85,7 @@ const TotalAmount = styled.div` align-items: center; } - .totalAmountText01 { + .totalAmountText { flex: 8 0 0; } diff --git a/client/src/components/StockOrderSection/StockOrderSetting.tsx b/client/src/components/StockOrderSection/StockOrderSetting.tsx index 1fa44830..ab31c923 100644 --- a/client/src/components/StockOrderSection/StockOrderSetting.tsx +++ b/client/src/components/StockOrderSection/StockOrderSetting.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import useGetStockInfo from "../../hooks/useGetStockInfo"; import { orderTypeBuying, orderTypeSelling } from "../../reducer/StockOrderType-Reducer"; @@ -19,6 +20,8 @@ const StockOrderSetting = () => { const companyId = useSelector((state: StateProps) => state.companyId); const { stockInfo, stockInfoLoading, stockInfoError } = useGetStockInfo(companyId); + const [orderVolume, setOrderVolume] = useState(0); + if (stockInfoLoading) { return <>>; } @@ -45,17 +48,18 @@ const StockOrderSetting = () => { {orderType02} - + - - + + ); }; export default StockOrderSetting; -const DecorationLine = () => { +// 매수/매도 탭 전환 시 하단에 시각화 되는 선 +const OrderTypeChangeEffetLine = () => { const stockOrderType = useSelector((state: StateProps) => state.stockOrderType); return ( diff --git a/client/src/components/StockOrderSection/StockPrice.tsx b/client/src/components/StockOrderSection/StockPrice.tsx index 5c71e6de..a3a0b8d0 100644 --- a/client/src/components/StockOrderSection/StockPrice.tsx +++ b/client/src/components/StockOrderSection/StockPrice.tsx @@ -7,6 +7,8 @@ import { styled } from "styled-components"; import { setStockOrderPrice } from "../../reducer/StockOrderPrice-Reducer"; import { StateProps } from "../../models/stateProps"; +const changeRateUnit = `%`; + const StockPrice = (props: StockPriceProps) => { const { index, price, volume, totalSellingVolume, totalBuyingVolum } = props; @@ -62,13 +64,16 @@ const StockPrice = (props: StockPriceProps) => { } // 전날 종가대비 매도/매수호가 변동률 - const changeRate = `${(((price - previousDayStockClosingPrice) / previousDayStockClosingPrice) * 100).toFixed(2)}%`; + const changeRate = (((price - previousDayStockClosingPrice) / previousDayStockClosingPrice) * 100).toFixed(2); return ( - + {price.toLocaleString()} - {changeRate} + + {changeRate} + {changeRateUnit} + {volume.toLocaleString()} @@ -118,7 +123,7 @@ const Container = styled.div<{ index: number; price: number; orderPrice: number } `; -const Price = styled.div` +const Price = styled.div<{ changeRate: number }>` width: 50%; display: flex; padding-right: 11px; @@ -134,7 +139,7 @@ const Price = styled.div` .changeRate { font-size: 12px; font-weight: 400; - color: #e22926; + color: ${(props) => (props.changeRate > 0 ? "#ed2926" : props.changeRate === 0 ? "black" : "#3177d7")}; padding-top: 1px; } `; diff --git a/client/src/components/StockOrderSection/StockPriceList.tsx b/client/src/components/StockOrderSection/StockPriceList.tsx index 89f24906..0bb5d618 100644 --- a/client/src/components/StockOrderSection/StockPriceList.tsx +++ b/client/src/components/StockOrderSection/StockPriceList.tsx @@ -7,6 +7,7 @@ import { StateProps } from "../../models/stateProps"; import StockPrice from "./StockPrice"; const StockPriceList = () => { + const stockOrderType = useSelector((state: StateProps) => state.stockOrderType); const companyId = useSelector((state: StateProps) => state.companyId); const { stockInfo, stockInfoLoading, stockInfoError } = useGetStockInfo(companyId); @@ -43,7 +44,7 @@ const StockPriceList = () => { } /* - [문제점] 주가 리스트 개수가 너무 적음 (매도호가 5개 + 매수호가 5개 = 총 10개) → 더미데이터를 추가하여 가격 리스트 확장 (매도 10개 + 매수 10개 = 총 20개) + [문제점] 주가 리스트 개수가 너무 적음 (매도호가 5개 + 매수호가 5개 = 총 10개) → UX를 저해하는 요소로 판단되어, 더미데이터를 추가 (매도/매수 각각 5개씩) [해결방안] 1) fetching 해온 데이터 중 가격 0인 데이터 제외 (한국투자증권 API에서 간혹 보내는 경우 있음) → 호가 간격 계산 후, 더미 데이터 추가 (거래량은 0으로 설정) */ const existSellingPrice = sellingPrice.filter((selling) => selling.price !== 0); @@ -70,20 +71,20 @@ const StockPriceList = () => { }, 0); return ( - - - - - + + + 매도호가 + 거래량 + {sellingAndBuyingPrice.map((item, idx) => ( ))} - - - - + + 매수호가 + 거래량 + ); }; @@ -97,31 +98,45 @@ interface PriceProps { } // component 생성 -const Container = styled.div` +const Container = styled.div<{ orderType: boolean }>` width: 40%; height: 100%; margin-right: 16px; -`; -const HighFigure = styled.div` - width: 100%; - height: 32px; - border-bottom: 1px solid black; + .priceIndicator { + display: flex; + flex-direction: row; + width: 100%; + height: 32px; + font-size: 13px; + padding-left: 15px; + + & div { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + .sellingPrice, + .sellingVolume { + color: ${(props) => (props.orderType ? "#9999" : "#e22926")}; + } + + .buyingPrice, + .buyingVolume { + color: ${(props) => (props.orderType ? "#2679ed" : "#9999")}; + } + } `; const PriceList = styled.ul` width: 100%; height: 348px; padding: 0px; - border-bottom: 1px solid black; overflow-y: scroll; &::-webkit-scrollbar { display: none; } `; - -const LowerFigure = styled.div` - width: 100%; - height: 32px; -`; diff --git a/client/src/components/StockOrderSection/VolumeSetteing.tsx b/client/src/components/StockOrderSection/VolumeSetteing.tsx index d364e2c5..a5188074 100644 --- a/client/src/components/StockOrderSection/VolumeSetteing.tsx +++ b/client/src/components/StockOrderSection/VolumeSetteing.tsx @@ -1,42 +1,114 @@ +import { useEffect } from "react"; +import { useSelector } from "react-redux"; import { styled } from "styled-components"; +import { StateProps } from "../../models/stateProps"; const volumeSettingTitle: string = "수량"; const maximumVolumeText01: string = "최대"; -const maximumVolumeText02: string = "주"; -const unitText: string = "주"; +const volumeUnit: string = "주"; -const percentageBtnText01: string = "10%"; -const percentageBtnText02: string = "25%"; -const percentageBtnText03: string = "50%"; -const percentageBtnText04: string = "100%"; +const volumPercentage01: number = 10; +const volumPercentage02: number = 25; +const volumPercentage03: number = 50; +const volumPercentage04: number = 100; +const percentageUnit: string = "%"; // dummyData -const dummyMaximum: number = 203; +const dummyMoney: number = 10000000; +const dummyholdingStock = 10; + +const VolumeSetting = (props: OwnProps) => { + const { orderVolume, setOrderVolume } = props; + const orderType = useSelector((state: StateProps) => state.stockOrderType); + const orderPrice = useSelector((state: StateProps) => state.stockOrderPrice); + const maximumBuyingVolume = Math.trunc(dummyMoney / orderPrice); + + const handlePlusOrderVolume = () => { + // 매수 -> 증가 버튼 클릭 시, 최대 구매수량 보다 낮으면 개수 1증가 + if (!orderType) { + orderVolume < maximumBuyingVolume && setOrderVolume((previousState: number) => previousState + 1); + } + // 매도 -> 증가 버튼 클릭 시, 보유 주식수량 보다 낮으면 개수 1증가 + if (orderType) { + orderVolume < dummyholdingStock && setOrderVolume((previousState: number) => previousState + 1); + } + }; + + const handleMinusOrderVolume = () => { + if (0 < orderVolume) { + setOrderVolume((previousState: number) => previousState - 1); + } + }; + + const handleSetVolumePercentage = (volumePerentage: number) => { + // 매수 -> percentage 버튼 클릭 시, 최대 구매수량 내에서 계산 + if (!orderType) { + const orderVolume = Math.trunc(maximumBuyingVolume * (volumePerentage / 100)); + setOrderVolume((previousState) => { + previousState = orderVolume; + return previousState; + }); + } + + // 매도 -> percentage 버튼 클릭 시, 보유 주식수량 내에서 계산 + if (orderType) { + const orderVolume = Math.trunc(dummyholdingStock * (volumePerentage / 100)); + setOrderVolume((previousState) => { + previousState = orderVolume; + return previousState; + }); + } + }; + + // 지정가 증가 -> (현재 주문수량 > 최대 주문가능 수량)일 경우 -> 현재 주문수량을 최대 주문수량으로 변경 + useEffect(() => { + if (maximumBuyingVolume < orderVolume) { + setOrderVolume((previousState) => { + previousState = maximumBuyingVolume; + return previousState; + }); + } + }, [maximumBuyingVolume]); -const VolumeSetting = () => { return ( - + {volumeSettingTitle} {maximumVolumeText01} - {dummyMaximum} - {maximumVolumeText02} + {orderType ? dummyholdingStock : maximumBuyingVolume} + {volumeUnit} - - {unitText} + + {volumeUnit} - ⋀ - ⋁ + + ⋀ + + + ⋁ + - {percentageBtnText01} - {percentageBtnText02} - {percentageBtnText03} - {percentageBtnText04} + handleSetVolumePercentage(volumPercentage01)}> + {volumPercentage01} + {percentageUnit} + + handleSetVolumePercentage(volumPercentage02)}> + {volumPercentage02} + {percentageUnit} + + handleSetVolumePercentage(volumPercentage03)}> + {volumPercentage03} + {percentageUnit} + + handleSetVolumePercentage(volumPercentage04)}> + {volumPercentage04} + {percentageUnit} + ); @@ -44,13 +116,20 @@ const VolumeSetting = () => { export default VolumeSetting; +// type 정의 +interface OwnProps { + orderVolume: number; + setOrderVolume: (updateFunction: (previousState: number) => number) => void; +} + +// component 생성 const Container = styled.div` width: 100%; margin-top: 16px; margin-bottom: 46px; `; -const TitleContainer = styled.div` +const TitleContainer = styled.div<{ orderType: boolean }>` display: flex; flex-direction: row; justify-content: space-between; @@ -73,7 +152,7 @@ const TitleContainer = styled.div` } .maximumVolume { - color: #ed2926; + color: ${(props) => (props.orderType ? "#3177d7" : "#ed2926")}; } } `; diff --git a/client/src/reducer/StockOrderType-Reducer.ts b/client/src/reducer/StockOrderType-Reducer.ts index 9999de9e..69264d9b 100644 --- a/client/src/reducer/StockOrderType-Reducer.ts +++ b/client/src/reducer/StockOrderType-Reducer.ts @@ -1,5 +1,6 @@ import { createSlice } from "@reduxjs/toolkit"; +// [true] 매도, [false] 매수 const initialState: boolean = false; const stockOrderTypeSlice = createSlice({