diff --git a/client/package-lock.json b/client/package-lock.json index 0ba4f0e5..1bd60132 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,11 +8,13 @@ "name": "vite-project", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^1.9.5", "axios": "^1.5.0", "echarts": "^5.4.3", "echarts-for-react": "^3.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^8.1.2", "styled-components": "^6.0.7" }, "devDependencies": { @@ -2487,6 +2489,38 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -2504,14 +2538,12 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "18.2.21", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2522,7 +2554,7 @@ "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -2530,8 +2562,7 @@ "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "node_modules/@types/semver": { "version": "7.5.1", @@ -2544,6 +2575,11 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.5.0.tgz", @@ -3823,6 +3859,19 @@ "node": ">=4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3832,6 +3881,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4387,6 +4445,49 @@ "react": "^18.2.0" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-redux": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -4408,6 +4509,22 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -4472,6 +4589,11 @@ "jsesc": "bin/jsesc" } }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", @@ -4929,6 +5051,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "4.4.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", diff --git a/client/package.json b/client/package.json index 204529df..51346128 100644 --- a/client/package.json +++ b/client/package.json @@ -10,11 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^1.9.5", "axios": "^1.5.0", "echarts": "^5.4.3", "echarts-for-react": "^3.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^8.1.2", "styled-components": "^6.0.7" }, "devDependencies": { diff --git a/client/src/components/StockOrderSection/Index.tsx b/client/src/components/StockOrderSection/Index.tsx new file mode 100644 index 00000000..b3a7359c --- /dev/null +++ b/client/src/components/StockOrderSection/Index.tsx @@ -0,0 +1,63 @@ +import { styled } from "styled-components"; + +import StockName from "./StockName"; +import OrderRequest from "./OrderRequest"; +import OrderResult from "./OrderResult"; + +const titleText: string = "주식주문"; + +const StockOrderSection = () => { + return ( + + + {titleText} + ✕ + + + + + + ); +}; + +export default StockOrderSection; + +const Container = styled.aside` + position: fixed; + right: -500px; + transition: right 0.3s ease-in-out; + display: flex; + flex-direction: column; + width: 26%; + min-width: 400px; + height: 100%; + background-color: #ffffff; +`; + +const UpperBar = styled.div` + position: relative; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + min-height: 43px; + border-bottom: 1px solid black; +`; + +const Title = styled.h2` + font-size: 17px; + font-weight: 450; + color: #1c1c1c; +`; + +const CloseBtn = styled.button` + position: absolute; + right: 10px; + width: 28px; + height: 95%; + border: none; + font-size: 20px; + color: #525252; + background-color: #ffff; +`; diff --git a/client/src/components/StockOrderSection/OrderRequest.tsx b/client/src/components/StockOrderSection/OrderRequest.tsx new file mode 100644 index 00000000..ae80f801 --- /dev/null +++ b/client/src/components/StockOrderSection/OrderRequest.tsx @@ -0,0 +1,23 @@ +import { styled } from "styled-components"; + +import StockPrice from "./StockPrice"; +import StockOrderSetting from "./StockOrderSetting"; + +const OrderRequest = () => { + return ( + + + + + ); +}; + +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/OrderResult.tsx b/client/src/components/StockOrderSection/OrderResult.tsx new file mode 100644 index 00000000..61e87f86 --- /dev/null +++ b/client/src/components/StockOrderSection/OrderResult.tsx @@ -0,0 +1,51 @@ +import { styled } from "styled-components"; + +const titleText: string = "주문내역"; +const orderPendingTitle: string = "미체결"; +const orderPendingEmptyMessage: string = "미체결 내역이 없습니다"; + +const OrderResult = () => { + return ( + + {titleText} + + {orderPendingTitle} + {orderPendingEmptyMessage} + + + ); +}; + +export default OrderResult; + +const Container = styled.div` + flex: 1 0 0; + padding-top: 16px; + display: flex; + flex-direction: column; +`; + +const Title = styled.div` + font-size: 16px; + font-weight: 500; + padding-left: 16px; + padding-bottom: 16px; +`; + +const OrderPending = styled.div` + .orderPendingTitle { + padding-left: 16px; + margin-bottom: 8px; + } + + .emptyIndicator { + width: 100%; + height: 48px; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + font-weight: 350; + color: #999999; + } +`; diff --git a/client/src/components/StockOrderSection/PriceSetting.tsx b/client/src/components/StockOrderSection/PriceSetting.tsx new file mode 100644 index 00000000..98a01aaa --- /dev/null +++ b/client/src/components/StockOrderSection/PriceSetting.tsx @@ -0,0 +1,181 @@ +import { useSelector, useDispatch } from "react-redux"; +import { styled } from "styled-components"; +import { setSpecifiedPrice, setMarketPrice } from "../../reducer/stockPriceType-Reducer"; +import { plusStockOrderPrice, minusStockOrderPrice } from "../../reducer/StockOrderPrice-Reducer"; +import { StateProps } from "../../models/stateProps"; + +const priceSettingTitle: string = "가격"; +const specifiedPriceBtnText: string = "지정가"; +const marketPriceBtnText: string = "시장가"; +const unitText: string = "원"; + +const PriceSetting = () => { + const priceType = useSelector((state: StateProps) => state.stockPriceType); + const orderPrice = useSelector((state: StateProps) => state.stockOrderPrice); + const dispatch = useDispatch(); + + // 시장가, 지정가 변경 + const handleSetSepcifiedPrice = () => { + dispatch(setSpecifiedPrice()); + }; + + const handleSetMarketPrice = () => { + dispatch(setMarketPrice()); + }; + + // 거래가 증가/감소 + const handlePlusOrderPrice = () => { + dispatch(plusStockOrderPrice(10)); + }; + + const handleMinusOrderPrice = () => { + dispatch(minusStockOrderPrice(10)); + }; + + return ( + + + {priceSettingTitle} + + + {specifiedPriceBtnText} + + + {marketPriceBtnText} + + + + + + {unitText} + + + ⋀ + + + ⋁ + + + + + ); +}; + +export default PriceSetting; + +// type 정의 +interface PriceTypeProps { + priceType: boolean; +} + +// component 생성 +const Container = styled.div` + width: 100%; + margin-top: 16px; + margin-bottom: 32px; +`; + +const PriceCategoryBox = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 8px; +`; + +const Title = styled.div` + padding-left: 5px; + font-size: 13px; + color: #999999; +`; + +const ButtonContainer = styled.div` + position: relative; + width: 100px; + height: 25px; + background-color: #f2f2f2; + border-radius: 0.3rem; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 2px; +`; + +const SepcifiedPriceBtn = styled.button` + width: 46px; + height: 21px; + border: none; + box-shadow: ${(props) => !props.priceType && "0.7px 0.7px 3px rgba(0, 0, 0, 0.4);"}; + border-radius: 0.3rem; + background-color: ${(props) => (props.priceType ? "f2f2f2" : "white")}; + color: ${(props) => (props.priceType ? "#999999" : "black")}; + + font-size: 13px; +`; + +const MarketPriceBtn = styled.button` + width: 46px; + height: 21px; + border: none; + border-radius: 0.3rem; + box-shadow: ${(props) => props.priceType && "0.7px 0.7px 3px rgba(0, 0, 0, 0.4);"}; + background-color: ${(props) => (props.priceType ? "white" : "f2f2f2")}; + color: ${(props) => (props.priceType ? "black" : "#999999")}; + font-size: 13px; +`; + +const PriceSettingBox = styled.div` + display: flex; + flex-direction: row; +`; + +const PriceController = styled.input` + flex: 1 0 0; + height: 30px; + border: 1px solid darkgray; + border-right: none; + border-radius: 0.2rem 0 0 0.2rem; + font-size: 15px; + font-weight: 500; + text-align: right; + padding-bottom: 3px; +`; + +const UnitContent = styled.div` + height: 30px; + color: #999999; + font-size: 13px; + font-weight: 400; + display: flex; + justify-content: center; + align-items: center; + padding-right: 8px; + border-top: 1px solid darkgray; + border-bottom: 1px solid darkgray; + background-color: #ffffff; +`; + +const DirectionBox = styled.div` + display: flex; + flex-direction: column; + + & button { + width: 31px; + height: 15px; + display: flex; + justify-content: center; + align-items: center; + font-size: 10px; + border: 1px solid darkgray; + border-radius: 0%; + + &.PriceUp { + border-bottom: none; + border-radius: 0 0.2rem 0 0; + } + + &.PriceDown { + border-radius: 0 0 0.2rem 0; + } + } +`; diff --git a/client/src/components/StockOrderSection/StockName.tsx b/client/src/components/StockOrderSection/StockName.tsx new file mode 100644 index 00000000..8b8b75cc --- /dev/null +++ b/client/src/components/StockOrderSection/StockName.tsx @@ -0,0 +1,58 @@ +import { styled } from "styled-components"; + +// dummyData +import logoImg from "../../asset/CentralSectionMenu-dummyImg.png"; +const corpName: string = "카카오"; +const stockCode: string = "035720"; +const marketType: string = "코스피"; + +const StockName = () => { + return ( + + + + {corpName} + + {stockCode} {marketType} + + + + ); +}; + +export default StockName; + +const Container = styled.section` + width: 100%; + height: 64px; + display: flex; + flex-direction: row; + align-items: center; + padding-top: 16px; + padding-bottom: 8px; + padding-left: 16px; + gap: 9px; + border-bottom: 1px solid black; +`; + +const CorpLogo = styled.img` + width: 28px; + height: 28px; + border-radius: 50%; +`; + +const NameContainer = styled.div` + height: 40px; + display: flex; + flex-direction: column; +`; + +const CorpName = styled.div` + font-size: 16px; + font-weight: 500; + color: #1c1c1c; +`; +const StockCode = styled.div` + font-size: 14px; + color: #999999; +`; diff --git a/client/src/components/StockOrderSection/StockOrderBtn.tsx b/client/src/components/StockOrderSection/StockOrderBtn.tsx new file mode 100644 index 00000000..394fed40 --- /dev/null +++ b/client/src/components/StockOrderSection/StockOrderBtn.tsx @@ -0,0 +1,90 @@ +import { useSelector } from "react-redux"; +import { styled } from "styled-components"; +import { StateProps } from "../../models/stateProps"; +import { OrderTypeProps } from "../../models/orderTypeProps"; + +const availableMoneyText01: string = "최대"; +const availableMoneyText02: string = "원"; +const totalAmountText01: string = "주문총액"; +const totalAmountText02: string = "원"; + +// dummyData +import { availableMoney } from "./dummyData"; +const dummyAmount: string = "0"; +const dummyMoney = availableMoney.toLocaleString(); + +const StockOrderBtn = () => { + const stockOrderType = useSelector((state: StateProps) => state.stockOrderType); + const orderBtnText: string = stockOrderType ? "매도" : "매수"; + + return ( + + + {availableMoneyText01} + {dummyMoney} + {availableMoneyText02} + + + {totalAmountText01} + {dummyAmount} + {totalAmountText02} + + {orderBtnText} + + ); +}; + +export default StockOrderBtn; + +const Container = styled.div``; + +const AvailableMoney = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-end; + gap: 4px; + + & span { + font-size: 14px; + color: #999999; + } + + .availableMoney { + color: #ed2926; + } +`; + +const TotalAmount = styled.div` + display: flex; + flex-direction: row; + margin-top: 4px; + gap: 4px; + + & div { + font-size: 13px; + color: #999999; + display: flex; + align-items: center; + } + + .totalAmountText01 { + flex: 8 0 0; + } + + .totalAmount { + color: black; + font-size: 15.5px; + } +`; + +const OrderBtn = styled.button` + width: 100%; + height: 32px; + margin-top: 16px; + border: none; + border-radius: 0.25rem; + background-color: ${(props) => (props.ordertype ? "#2679ed" : "#e22926")}; + transition: background-color 0.5s; + color: #ffffff; + font-weight: 400; +`; diff --git a/client/src/components/StockOrderSection/StockOrderSetting.tsx b/client/src/components/StockOrderSection/StockOrderSetting.tsx new file mode 100644 index 00000000..52ec57d1 --- /dev/null +++ b/client/src/components/StockOrderSection/StockOrderSetting.tsx @@ -0,0 +1,109 @@ +import { useSelector, useDispatch } from "react-redux"; +import { orderTypeBuying, orderTypeSelling } from "../../reducer/StockOrderType-Reducer"; +import { styled } from "styled-components"; +import { StateProps } from "../../models/stateProps"; +import { OrderTypeProps } from "../../models/orderTypeProps"; + +import PriceSetting from "./PriceSetting"; +import VolumeSetting from "./VolumeSetteing"; +import StockOrderBtn from "./StockOrderBtn"; + +const orderType01: string = "매수"; +const orderType02: string = "매도"; + +const StockOrderSetting = () => { + const stockOrderType = useSelector((state: StateProps) => state.stockOrderType); + const dispatch = useDispatch(); + + const handleSetBuying = () => { + dispatch(orderTypeBuying()); + }; + + const handleSetSelling = () => { + dispatch(orderTypeSelling()); + }; + + return ( + + + + {orderType01} + + + {orderType02} + + + + + + + + ); +}; + +export default StockOrderSetting; + +const OrderTypeDividingLine = () => { + const stockOrderType = useSelector((state: StateProps) => state.stockOrderType); + + return ( + + + + + + ); +}; + +// component 생성 +const Container = styled.div` + width: 51%; + height: 100%; +`; + +const OrderType = styled.div` + width: 100%; + height: 31px; + display: flex; + flex-direction: row; + color: #9999; +`; + +const Buying = styled.div` + flex: 1 0 0; + display: flex; + justify-content: center; + align-items: center; + height: 31px; + font-size: 14px; + color: ${(props) => !props.ordertype && "#e22926"}; + transition: color 0.5s; +`; + +const Selling = styled.div` + flex: 1 0 0; + display: flex; + justify-content: center; + align-items: center; + height: 31px; + font-size: 14px; + color: ${(props) => props.ordertype && "#2679ed"}; + transition: color 0.5s; +`; + +const DividingContainer = styled.div` + background-color: darkgray; +`; + +const DefaultLine = styled.div` + transform: translateX(${(props) => (props.ordertype ? "50%" : "0")}); + transition: transform 0.3s ease-in-out; + width: 100%; + height: 2px; +`; + +const DivdingLine = styled.div` + width: 50%; + height: 2px; + background-color: ${(props) => (props.ordertype ? "#2679ed" : "#e22926")}; +`; diff --git a/client/src/components/StockOrderSection/StockPrice.tsx b/client/src/components/StockOrderSection/StockPrice.tsx new file mode 100644 index 00000000..19c15345 --- /dev/null +++ b/client/src/components/StockOrderSection/StockPrice.tsx @@ -0,0 +1,198 @@ +import { useSelector, useDispatch } from "react-redux"; +import { useRef, useEffect } from "react"; +import { styled } from "styled-components"; +import { setStockOrderPrice } from "../../reducer/StockOrderPrice-Reducer"; +import { StateProps } from "../../models/stateProps"; + +// dummyData +import { dummyPrice } from "./dummyData"; +import { upperPriceVolumeSum, lowerPriceVolumeSum } from "./dummyData"; + +const StockPrice = () => { + return ( + + + + + + + {dummyPrice.map((item, idx) => ( + + ))} + + + + + + + ); +}; + +export default StockPrice; + +const PriceInfo = (props: PriceInfoProps) => { + const { index, price, changeRate, volume } = props; + const ref = useRef(null); + + const stockOrderPrice = useSelector((state: StateProps) => state.stockOrderPrice); + const dispatch = useDispatch(); + const handleSetOrderPrice = () => { + dispatch(setStockOrderPrice(price)); + }; + + const changeRateText01: string = changeRate > 0 ? "+" : ""; + const changeRateText02: string = "%"; + + // 11번째 가격 -> 렌더링 시 정중앙에 위치하도록 + useEffect(() => { + ref.current?.focus(); + ref.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, []); + + if (index === 10) { + return ( + + + {price} + + {changeRateText01} + {changeRate} + {changeRateText02} + + + + {volume} + + + + ); + } + + return ( + + + {price} + + {changeRateText01} + {changeRate} + {changeRateText02} + + + + {volume} + + + + ); +}; + +// type 지정 +interface PriceInfoProps { + index: number; + price: number; + changeRate: number; + volume: number; +} + +interface InfoContainerProps { + index: number; + price: number; + orderPrice: number; +} + +interface VolumeProps { + index: number; +} + +interface VolumePercentageProps { + index: number; + volume: number; + upperPriceVolumeSum: number; + lowerPriceVolumeSum: number; +} + +// component 생성 +const Container = styled.div` + width: 40%; + height: 100%; + margin-right: 16px; +`; + +const HighFigure = styled.div` + width: 100%; + height: 32px; + border-bottom: 1px solid black; +`; + +const PriceList = styled.ul` + width: 100%; + height: 348px; + padding: 0px; + border-bottom: 1px solid black; + overflow-y: scroll; + + &::-webkit-scrollbar { + display: none; + } +`; + +const InfoContainer = styled.div` + width: 100%; + height: 36px; + margin-bottom: 2px; + background-color: ${(props) => (props.index > 9 ? "#FDE8E7" : "#E7F0FD")}; + border: ${(props) => (props.index === 10 ? "1px solid #2F4F4F" : "none")}; + border-left: ${(props) => (props.price === props.orderPrice ? "3px solid red" : props.index > 9 ? "3px solid #FDE8E7" : "3px solid #E7F0FD")}; + display: flex; + flex-direction: row; +`; + +const Price = styled.div` + width: 50%; + display: flex; + padding-right: 11px; + flex-direction: column; + align-items: flex-end; + + .price { + font-size: 14px; + font-weight: 400; + padding-top: 1px; + } + + .changeRate { + font-size: 12px; + font-weight: 400; + color: #e22926; + padding-top: 1px; + } +`; + +const Volume = styled.div` + width: 50%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + font-size: 12px; + color: ${(props) => (props.index < 10 ? "#2679ed" : "#e22926")}; + + .volume { + height: 100%; + display: flex; + align-items: center; + padding-right: 8px; + } +`; + +const VolumePercentge = styled.span` + width: ${(props) => (props.volume / (props.index < 10 ? props.upperPriceVolumeSum : props.lowerPriceVolumeSum)) * 100}%; + + height: 2px; + background-color: ${(props) => (props.index < 10 ? "#2679ed" : "#e22926")}; +`; + +const LowerFigure = styled.div` + width: 100%; + height: 32px; +`; diff --git a/client/src/components/StockOrderSection/VolumeSetteing.tsx b/client/src/components/StockOrderSection/VolumeSetteing.tsx new file mode 100644 index 00000000..b78300ae --- /dev/null +++ b/client/src/components/StockOrderSection/VolumeSetteing.tsx @@ -0,0 +1,129 @@ +import { styled } from "styled-components"; + +const volumeSettingTitle: string = "수량"; +const maximumVolumeText01: string = "최대"; +const maximumVolumeText02: string = "주"; + +const percentageBtnText01: string = "10%"; +const percentageBtnText02: string = "25%"; +const percentageBtnText03: string = "50%"; +const percentageBtnText04: string = "100%"; + +// dummyData +const dummyMaximum: number = 203; + +const VolumeSetting = () => { + return ( + + + {volumeSettingTitle} + + {maximumVolumeText01} + {dummyMaximum} + {maximumVolumeText02} + + + + + + ⋀ + ⋁ + + + + {percentageBtnText01} + {percentageBtnText02} + {percentageBtnText03} + {percentageBtnText04} + + + ); +}; + +export default VolumeSetting; + +const Container = styled.div` + width: 100%; + margin-top: 16px; + margin-bottom: 46px; +`; + +const TitleContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 8px; +`; + +const Title = styled.div` + padding-left: 5px; + font-size: 13px; + color: #999999; +`; + +const MaximumVolumeBox = styled.div` + display: flex; + flex-direction: row; + gap: 3px; + + & span { + font-size: 14px; + color: #999999; + } + + .maximumVolume { + color: #ed2926; + } +`; + +const VolumeSettingBox = styled.div` + display: flex; + flex-direction: row; +`; + +const VolumeController = styled.input` + flex: 1 0 0; + height: 30px; + border: 1px solid darkgray; + border-right: none; + border-radius: 0.2rem 0 0 0.2rem; +`; + +const DirectionBox = styled.div` + display: flex; + flex-direction: column; + + & button { + width: 31px; + height: 15px; + display: flex; + justify-content: center; + align-items: center; + font-size: 10px; + border: 1px solid darkgray; + border-radius: 0%; + + &.VolumeUp { + border-bottom: none; + border-radius: 0 0.2rem 0 0; + } + + &.VolumeDown { + border-radius: 0 0 0.2rem 0; + } + } +`; + +const PercentageBox = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 8px; + + & button { + width: 56px; + height: 28px; + border: none; + border-radius: 0.2rem; + } +`; diff --git a/client/src/components/StockOrderSection/dummyData.ts b/client/src/components/StockOrderSection/dummyData.ts new file mode 100644 index 00000000..285cf917 --- /dev/null +++ b/client/src/components/StockOrderSection/dummyData.ts @@ -0,0 +1,34 @@ +// dummyData +export const dummyPrice: dummyProps[] = [ + { price: 200, changeRate: 90, volume: 300 }, + { price: 190, changeRate: 90, volume: 500 }, + { price: 180, changeRate: 80, volume: 120 }, + { price: 170, changeRate: 70, volume: 78 }, + { price: 160, changeRate: 60, volume: 55 }, + { price: 150, changeRate: 50, volume: 91 }, + { price: 140, changeRate: 40, volume: 300 }, + { price: 130, changeRate: 30, volume: 10 }, + { price: 120, changeRate: 20, volume: 80 }, + { price: 110, changeRate: 10, volume: 40 }, + { price: 100, changeRate: 0, volume: 180 }, + { price: 90, changeRate: -10, volume: 250 }, + { price: 80, changeRate: -20, volume: 1000 }, + { price: 70, changeRate: -30, volume: 900 }, + { price: 60, changeRate: -40, volume: 850 }, + { price: 50, changeRate: -50, volume: 154 }, + { price: 40, changeRate: -60, volume: 820 }, + { price: 30, changeRate: -70, volume: 1100 }, + { price: 20, changeRate: -80, volume: 800 }, + { price: 10, changeRate: -90, volume: 500 }, +]; +export const standardPrice = 100; +export const upperPriceVolumeSum = 1000; +export const lowerPriceVolumeSum = 2000; +export const availableMoney = 10000000; + +// dummy 관련 변수 +interface dummyProps { + price: number; + changeRate: number; + volume: number; +} diff --git a/client/src/main.tsx b/client/src/main.tsx index 95e2bdc2..dc075919 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,9 +1,13 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; +import { Provider } from "react-redux"; +import store from "./store/config.ts"; ReactDOM.createRoot(document.getElementById("root")!).render( - + + + ); diff --git a/client/src/models/README.md b/client/src/models/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/models/orderTypeProps.ts b/client/src/models/orderTypeProps.ts new file mode 100644 index 00000000..c50c2081 --- /dev/null +++ b/client/src/models/orderTypeProps.ts @@ -0,0 +1,3 @@ +export interface OrderTypeProps { + ordertype: boolean; +} diff --git a/client/src/models/stateProps.ts b/client/src/models/stateProps.ts new file mode 100644 index 00000000..4d308885 --- /dev/null +++ b/client/src/models/stateProps.ts @@ -0,0 +1,5 @@ +export interface StateProps { + stockOrderType: boolean; + stockPriceType: boolean; + stockOrderPrice: number; +} diff --git a/client/src/page/MainPage.tsx b/client/src/page/MainPage.tsx index c73776a5..59c868c0 100644 --- a/client/src/page/MainPage.tsx +++ b/client/src/page/MainPage.tsx @@ -10,6 +10,7 @@ import PasswordSettingModal from "../components/Signups/Password"; import CentralChartSection from "../components/CentralChartSection/Index"; import CompareChartSection from "../components/CompareChartSection/Index"; +import StockOrderSection from "../components/StockOrderSection/Index"; const MainPage = () => { const [isOAuthModalOpen, setOAuthModalOpen] = useState(false); @@ -42,8 +43,7 @@ const MainPage = () => { setEmailSignupModalOpen(false); }, []); - const [isEmailVerificationModalOpen, setEmailVerificationModalOpen] = - useState(false); + const [isEmailVerificationModalOpen, setEmailVerificationModalOpen] = useState(false); const openEmailVerificationModal = useCallback(() => { setEmailSignupModalOpen(false); // 이메일 회원가입 모달 닫기 @@ -54,8 +54,7 @@ const MainPage = () => { setEmailVerificationModalOpen(false); }, []); - const [isPasswordSettingModalOpen, setPasswordSettingModalOpen] = - useState(false); + const [isPasswordSettingModalOpen, setPasswordSettingModalOpen] = useState(false); const openPasswordSettingModal = useCallback(() => { setEmailVerificationModalOpen(false); // 이메일 인증 모달 닫기 @@ -87,15 +86,10 @@ const MainPage = () => { + - {isOAuthModalOpen && ( - - )} + {isOAuthModalOpen && } {isEmailLoginModalOpen && ( { + return action.payload; + }, + plusStockOrderPrice: (state, action) => { + return state + action.payload; + }, + minusStockOrderPrice: (state, action) => { + if (state > action.payload) { + return state - action.payload; + } + return state; + }, + }, +}); + +export const { setStockOrderPrice, plusStockOrderPrice, minusStockOrderPrice } = stockPriceOrderSlice.actions; +export const stockOrderPriceReducer = stockPriceOrderSlice.reducer; diff --git a/client/src/reducer/StockOrderType-Reducer.ts b/client/src/reducer/StockOrderType-Reducer.ts new file mode 100644 index 00000000..9999de9e --- /dev/null +++ b/client/src/reducer/StockOrderType-Reducer.ts @@ -0,0 +1,19 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState: boolean = false; + +const stockOrderTypeSlice = createSlice({ + name: "stockOrderType", + initialState: initialState, + reducers: { + orderTypeBuying: () => { + return false; + }, + orderTypeSelling: () => { + return true; + }, + }, +}); + +export const { orderTypeBuying, orderTypeSelling } = stockOrderTypeSlice.actions; +export const stockOrderTypeReducer = stockOrderTypeSlice.reducer; diff --git a/client/src/reducer/StockPriceType-Reducer.ts b/client/src/reducer/StockPriceType-Reducer.ts new file mode 100644 index 00000000..1bb2062a --- /dev/null +++ b/client/src/reducer/StockPriceType-Reducer.ts @@ -0,0 +1,15 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState: boolean = false; + +const stockPriceTypeSlice = createSlice({ + name: "stockPriceType", + initialState: initialState, + reducers: { + setSpecifiedPrice: () => false, + setMarketPrice: () => true, + }, +}); + +export const { setSpecifiedPrice, setMarketPrice } = stockPriceTypeSlice.actions; +export const stockPriceTypeReducer = stockPriceTypeSlice.reducer; diff --git a/client/src/store/config.ts b/client/src/store/config.ts new file mode 100644 index 00000000..2a5b7c14 --- /dev/null +++ b/client/src/store/config.ts @@ -0,0 +1,14 @@ +import { configureStore } from "@reduxjs/toolkit"; +import { stockOrderTypeReducer } from "../reducer/StockOrderType-Reducer"; +import { stockPriceTypeReducer } from "../reducer/stockPriceType-Reducer"; +import { stockOrderPriceReducer } from "../reducer/StockOrderPrice-Reducer"; + +const store = configureStore({ + reducer: { + stockOrderType: stockOrderTypeReducer, + stockPriceType: stockPriceTypeReducer, + stockOrderPrice: stockOrderPriceReducer, + }, +}); + +export default store;