├── public ├── favicon.png ├── robots.txt ├── blueLogo.png ├── whiteLogo.png └── index.html ├── src ├── styles │ ├── fonts │ │ ├── NotoSansKR-Black.otf │ │ ├── NotoSansKR-Black.woff │ │ ├── NotoSansKR-Bold.otf │ │ ├── NotoSansKR-Bold.woff │ │ ├── NotoSansKR-Bold.woff2 │ │ ├── NotoSansKR-Light.otf │ │ ├── NotoSansKR-Light.woff │ │ ├── NotoSansKR-Medium.otf │ │ ├── NotoSansKR-Thin.otf │ │ ├── NotoSansKR-Thin.woff │ │ ├── NotoSansKR-Thin.woff2 │ │ ├── NotoSansKR-Black.woff2 │ │ ├── NotoSansKR-Light.woff2 │ │ ├── NotoSansKR-Medium.woff │ │ ├── NotoSansKR-Medium.woff2 │ │ ├── NotoSansKR-Regular.otf │ │ ├── NotoSansKR-Regular.woff │ │ ├── NotoSansKR-DemiLight.otf │ │ ├── NotoSansKR-DemiLight.woff │ │ ├── NotoSansKR-DemiLight.woff2 │ │ └── NotoSansKR-Regular.woff2 │ ├── GlobalStyle.js │ └── theme.js ├── setupTests.js ├── Container │ ├── withThemeData.js │ ├── withTradeListData.js │ ├── withOHLCData.js │ ├── withSize.js │ ├── withSelectedCoinName.js │ ├── withLoadingData.js │ ├── withLatestCoinData.js │ ├── withSelectedOption.js │ ├── withMarketNames.js │ ├── withOrderbookData.js │ └── withSelectedCoinPrice.js ├── Router │ └── MainRouter.js ├── App.js ├── Reducer │ ├── index.js │ ├── loadingReducer.js │ └── coinReducer.js ├── Components │ ├── Main │ │ ├── OrderInfoTradeList.js │ │ ├── OrderbookCoinInfo.js │ │ ├── MainChart-d3fc.js │ │ ├── TradeListItem.js │ │ ├── OrderInfo.js │ │ ├── ChartDataConsole.js │ │ ├── Orderbook.js │ │ ├── TradeList.js │ │ ├── OrderbookItem.js │ │ ├── MainChart-old.js │ │ ├── CoinListItem.js │ │ ├── CoinInfoHeader.js │ │ ├── CoinList.js │ │ ├── OrderInfoAskBid.js │ │ └── MainChart.js │ └── Global │ │ ├── Loading.js │ │ ├── Header.js │ │ └── Footer.js ├── index.css ├── index.js ├── Api │ └── api.js ├── Pages │ └── Main.js ├── serviceWorker.js └── Lib │ ├── asyncUtil.js │ └── utils.js ├── LICENSE ├── package.json ├── .gitignore └── README.md /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/blueLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/public/blueLogo.png -------------------------------------------------------------------------------- /public/whiteLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/public/whiteLogo.png -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Black.otf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Black.woff -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Bold.otf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Bold.woff -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Bold.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Light.otf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Light.woff -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Medium.otf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Thin.otf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Thin.woff -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Thin.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Black.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Light.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Medium.woff -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Medium.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Regular.otf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-DemiLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-DemiLight.otf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-DemiLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-DemiLight.woff -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-DemiLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-DemiLight.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/NotoSansKR-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seongkyun-Yu/upbit-downbit/HEAD/src/styles/fonts/NotoSansKR-Regular.woff2 -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/Container/withThemeData.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { ThemeContext } from "styled-components"; 3 | 4 | const withThemeData = () => (OriginalComponent) => (props) => { 5 | const theme = useContext(ThemeContext); // 테마 정보 6 | return ; 7 | }; 8 | 9 | export default withThemeData; 10 | -------------------------------------------------------------------------------- /src/Router/MainRouter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Switch, Route } from "react-router-dom"; 3 | import Main from "../Pages/Main"; 4 | 5 | const MainRouter = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default MainRouter; 15 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { startInit } from "./Reducer/coinReducer"; 4 | import MainRouter from "./Router/MainRouter"; 5 | 6 | function App() { 7 | const dispatch = useDispatch(); 8 | useEffect(() => { 9 | dispatch(startInit()); 10 | }, [dispatch]); 11 | 12 | return ; 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/Reducer/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { coinReducer, coinSaga } from "./coinReducer"; 3 | import { loadingReducer } from "./loadingReducer"; 4 | import { all } from "redux-saga/effects"; 5 | 6 | const rootReducer = combineReducers({ 7 | Coin: coinReducer, 8 | Loading: loadingReducer, 9 | }); 10 | 11 | function* rootSaga() { 12 | yield all([coinSaga()]); 13 | } 14 | 15 | export { rootReducer, rootSaga }; 16 | -------------------------------------------------------------------------------- /src/styles/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | import normalize from "styled-normalize"; 3 | import reset from "styled-reset"; 4 | 5 | const GlobalStyle = createGlobalStyle` 6 | ${normalize} 7 | ${reset} 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | background-color: rgb(231, 234, 239); 14 | /* height: 100%; */ 15 | } 16 | `; 17 | 18 | export default GlobalStyle; 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | Downbit 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Components/Main/OrderInfoTradeList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const St = { 5 | Container: styled.div` 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | width: 100%; 10 | height: 212px; 11 | background-color: white; 12 | font-size: 0.8rem; 13 | color: #666; 14 | `, 15 | }; 16 | 17 | const OrderInfoTradeList = ({ theme }) => { 18 | return 로그인 후 사용 가능합니다.; 19 | }; 20 | 21 | export default React.memo(OrderInfoTradeList); 22 | -------------------------------------------------------------------------------- /src/Container/withTradeListData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withTradeListData = () => (OriginalComponent) => (props) => { 5 | const selectedMarket = useSelector((state) => state.Coin.selectedMarket); 6 | const selectedTradeListData = useSelector( 7 | (state) => state.Coin.tradeList.data[selectedMarket] 8 | ); 9 | 10 | return ( 11 | 15 | ); 16 | }; 17 | 18 | export default withTradeListData; 19 | -------------------------------------------------------------------------------- /src/Container/withOHLCData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withOHLCData = () => (OriginalComponent) => () => { 5 | const selectedMarket = useSelector((state) => state.Coin.selectedMarket); // 선택된 코인/마켓 6 | const selectedCandles = useSelector( 7 | (state) => state.Coin.candle.data[selectedMarket].candles 8 | ); // 선택된 코인/마켓 캔들 정보 9 | 10 | // return selectedCandles.length ? ( 11 | // 12 | // ) : ( 13 | //
Chart Loading
14 | // ); 15 | return ; 16 | }; 17 | 18 | export default withOHLCData; 19 | -------------------------------------------------------------------------------- /src/Components/Global/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactLoading from "react-loading"; 3 | import styled, { css } from "styled-components"; 4 | 5 | const St = { 6 | Container: styled.div` 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | width: 100%; 11 | height: 100%; 12 | ${({ isCenter }) => 13 | !isCenter && 14 | css` 15 | align-items: stretch; 16 | margin-top: 200px; 17 | `} 18 | `, 19 | }; 20 | 21 | const Loading = ({ center = true }) => { 22 | return ( 23 | 24 | 30 | 31 | ); 32 | }; 33 | 34 | export default Loading; 35 | -------------------------------------------------------------------------------- /src/Container/withSize.js: -------------------------------------------------------------------------------- 1 | import { throttle } from "lodash"; 2 | import React, { useCallback, useEffect, useState } from "react"; 3 | 4 | const withSize = () => (OriginalComponent) => (props) => { 5 | const [widthSize, setWidthSize] = useState(window.innerWidth); 6 | const [heightSize, setHeightSize] = useState(window.innerHeight); 7 | 8 | const handleSize = useCallback(() => { 9 | setWidthSize(window.innerWidth); 10 | setHeightSize(window.innerHeight); 11 | }, []); 12 | 13 | useEffect(() => { 14 | window.addEventListener("resize", throttle(handleSize, 200)); 15 | return () => { 16 | window.removeEventListener("resize", handleSize); 17 | }; 18 | }, [handleSize]); 19 | 20 | return ( 21 | 26 | ); 27 | }; 28 | 29 | export default withSize; 30 | -------------------------------------------------------------------------------- /src/Container/withSelectedCoinName.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withSelectedCoinName = () => (OriginalComponent) => (props) => { 5 | const selectedMarket = useSelector((state) => state.Coin.selectedMarket); 6 | const coinNameKor = useSelector( 7 | (state) => state.Coin.marketNames.data[selectedMarket].korean 8 | ); 9 | const coinNameEng = useSelector( 10 | (state) => state.Coin.marketNames.data[selectedMarket].english 11 | ); 12 | 13 | const splitedName = selectedMarket.split("-"); 14 | const coinSymbol = splitedName[1]; 15 | const coinNameAndMarketEng = splitedName[1] + "/" + splitedName[0]; 16 | 17 | return ( 18 | 25 | ); 26 | }; 27 | 28 | export default withSelectedCoinName; 29 | -------------------------------------------------------------------------------- /src/styles/theme.js: -------------------------------------------------------------------------------- 1 | const viewSize = { 2 | mobileS: 480, 3 | mobileM: 770, 4 | tablet: 1279, 5 | desktop: 1280, 6 | }; 7 | 8 | const theme = { 9 | deepBlue: "#093687", 10 | skyBlue1: "rgba(0,98,223,.03)", 11 | skyBlue2: "rgba(0,98,223,.09)", 12 | lightPink1: "rgba(216,14,53,.03);", 13 | lightPink2: "rgba(216,14,53,.09);", 14 | strongRed: "#d80e35", 15 | strongBlue: "#115DCB", 16 | priceUp: "rgb(210, 79, 69)", 17 | priceDown: "rgb(18, 97, 196)", 18 | priceUpTrans: "rgba(210, 79, 69, 0.5)", 19 | priceDownTrans: "rgba(18, 97, 196, 0.5)", 20 | middleGray: "#00000033", 21 | lightGray: "rgb(244, 245, 248)", 22 | lightGray1: "rgb(249, 250, 252)", 23 | lightGray2: "rgb(212, 214, 220)", 24 | mobileS: `(max-width: ${viewSize.mobileS}px)`, 25 | mobileM: `(max-width: ${viewSize.mobileM}px)`, 26 | tablet: `(max-width: ${viewSize.tablet}px)`, 27 | desktop: `(min-width: ${viewSize.desktop}px)`, 28 | }; 29 | 30 | export { viewSize }; 31 | export default theme; 32 | -------------------------------------------------------------------------------- /src/Reducer/loadingReducer.js: -------------------------------------------------------------------------------- 1 | const START_LOADING = "loading/START_LOADING"; 2 | const FINISH_LOADING = "loading/FINISH_LOADING"; 3 | 4 | const startLoading = (payload) => ({ type: START_LOADING, payload }); 5 | const finishLoading = (payload) => ({ type: FINISH_LOADING, payload }); 6 | 7 | const initialState = { 8 | "coin/GET_ONE_COIN_CANDLES": true, 9 | "coin/GET_INIT_ORDERBOOKS": true, 10 | "coin/GET_ONE_COIN_TRADELISTS": true, 11 | "coin/GET_INIT_CANDLES": true, 12 | "coin/GET_MARKET_NAMES": true, 13 | "coin/GET_ADDITIONAL_COIN_CANDLES": false, 14 | }; 15 | 16 | const loadingReducer = (state = initialState, action) => { 17 | switch (action.type) { 18 | case START_LOADING: 19 | return { 20 | ...state, 21 | [action.payload]: true, 22 | }; 23 | case FINISH_LOADING: 24 | return { 25 | ...state, 26 | [action.payload]: false, 27 | }; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | export { startLoading, finishLoading, loadingReducer }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Seongkyun Yu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Container/withLoadingData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withLoadingData = () => (OriginalComponent) => (props) => { 5 | const isCandleLoading = useSelector( 6 | (state) => state.Loading["coin/GET_ONE_COIN_CANDLES"] 7 | ); 8 | const isOrderbookLoading = useSelector( 9 | (state) => state.Loading["coin/GET_INIT_ORDERBOOKS"] 10 | ); 11 | const isTradeListLoading = useSelector( 12 | (state) => state.Loading["coin/GET_ONE_COIN_TRADELISTS"] 13 | ); 14 | const isInitCandleLoading = useSelector( 15 | (state) => state.Loading["coin/GET_INIT_CANDLES"] 16 | ); 17 | const isMarketNamesLoading = useSelector( 18 | (state) => state.Loading["coin/GET_MARKET_NAMES"] 19 | ); 20 | 21 | return ( 22 | 30 | ); 31 | }; 32 | 33 | export default withLoadingData; 34 | -------------------------------------------------------------------------------- /src/Container/withLatestCoinData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withLatestCoinData = () => (OriginalComponent) => (props) => { 5 | const coinListDatas = useSelector((state) => state.Coin.candle.data); // 코인들 데이터 6 | 7 | const latestCoinData = {}; 8 | 9 | if (Object.keys(coinListDatas).length > 2) { 10 | Object.keys(coinListDatas).forEach((marketName) => { 11 | latestCoinData[marketName] = {}; 12 | latestCoinData[marketName].price = 13 | coinListDatas[marketName].candles[ 14 | coinListDatas[marketName].candles.length - 1 15 | ].close; 16 | 17 | latestCoinData[marketName].changeRate24Hour = ( 18 | Math.round(coinListDatas[marketName].changeRate24Hour * 10000) / 100 19 | ).toFixed(2); 20 | 21 | latestCoinData[marketName].changePrice24Hour = 22 | coinListDatas[marketName].changePrice24Hour; 23 | 24 | latestCoinData[marketName].tradePrice24Hour = Math.floor( 25 | coinListDatas[marketName].tradePrice24Hour / 1000000 26 | ); 27 | }); 28 | } 29 | 30 | return ; 31 | }; 32 | 33 | export default withLatestCoinData; 34 | -------------------------------------------------------------------------------- /src/Container/withSelectedOption.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withSelectedOption = () => (OriginalComponent) => (props) => { 5 | const selectedMarket = useSelector((state) => state.Coin.selectedMarket); 6 | const selectedTimeType = useSelector((state) => state.Coin.selectedTimeType); 7 | const selectedTimeCount = useSelector( 8 | (state) => state.Coin.selectedTimeCount 9 | ); 10 | const selectedAskBidOrder = useSelector( 11 | (state) => state.Coin.selectedAskBidOrder 12 | ); 13 | const searchCoin = useSelector((state) => state.Coin.searchCoin); 14 | const orderPrice = useSelector((state) => state.Coin.orderPrice); 15 | const orderAmount = useSelector((state) => state.Coin.orderAmount); 16 | const orderTotalPrice = useSelector((state) => state.Coin.orderTotalPrice); 17 | 18 | return ( 19 | 30 | ); 31 | }; 32 | 33 | export default withSelectedOption; 34 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: "Noto Sans CJK KR", sans-serif; 3 | } 4 | 5 | /* 6 | @font-face { 7 | font-family: "Noto Sans CJK KR"; 8 | font-style: normal; 9 | font-weight: 100; 10 | src: url("/src/styles/fonts/NotoSansKR-Light.woff2") format("woff2"), 11 | url("/src/styles/fonts/NotoSansKR-Light.woff") format("woff"), 12 | url("/src/styles/fonts/NotoSansKR-Light.otf") format("truetype"); 13 | } 14 | 15 | @font-face { 16 | font-family: "Noto Sans CJK KR"; 17 | font-style: normal; 18 | font-weight: normal; 19 | src: url("/src/styles/fonts/NotoSansKR-Regular.woff2") format("woff2"), 20 | url("/src/styles/fonts/NotoSansKR-Regular.woff") format("woff"), 21 | url("/src/styles/fonts/NotoSansKR-Regular.otf") format("truetype"); 22 | } 23 | 24 | @font-face { 25 | font-family: "Noto Sans CJK KR"; 26 | font-style: normal; 27 | font-weight: 500; 28 | src: url("/src/styles/fonts/NotoSansKR-Medium.woff2") format("woff2"), 29 | url("/src/styles/fonts/NotoSansKR-Medium.woff") format("woff"), 30 | url("/src/styles/fonts/NotoSansKR-Medium.otf") format("truetype"); 31 | } 32 | 33 | @font-face { 34 | font-family: "Noto Sans CJK KR"; 35 | font-style: normal; 36 | font-weight: 800; 37 | src: url("/src/styles/fonts/NotoSansKR-Bold.woff2") format("woff2"), 38 | url("/src/styles/fonts/NotoSansKR-Bold.woff") format("woff"), 39 | url("/src/styles/fonts/NotoSansKR-Bold.otf") format("truetype"); 40 | } */ 41 | -------------------------------------------------------------------------------- /src/Components/Global/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const St = { 5 | Header: styled.header` 6 | position: sticky; 7 | top: 0; 8 | z-index: 100; 9 | width: 100%; 10 | height: 60px; 11 | background-color: rgb(9, 54, 135); 12 | `, 13 | Container: styled.div` 14 | display: flex; 15 | align-items: center; 16 | width: 100%; 17 | height: 100%; 18 | max-width: 1360px; 19 | margin: 0 auto; 20 | 21 | @media ${({ theme, isRootURL }) => (!isRootURL ? theme.tablet : true)} { 22 | max-width: 950px; 23 | } 24 | 25 | @media ${({ theme, isRootURL }) => (isRootURL ? theme.tablet : true)} { 26 | max-width: 100%; 27 | } 28 | `, 29 | SiteHeading: styled.h1` 30 | padding: 0 20px; 31 | width: 150px; 32 | height: 100%; 33 | `, 34 | MainLink: styled.a` 35 | display: block; 36 | background-image: ${({ logo }) => `url(${logo})`}; 37 | background-repeat: no-repeat; 38 | background-position: center; 39 | background-size: contain; 40 | color: transparent; 41 | width: 100%; 42 | height: 100%; 43 | `, 44 | }; 45 | 46 | const Header = ({ isRootURL }) => { 47 | return ( 48 | 49 | 50 | 51 | 56 | 업비트 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default Header; 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "core-js/stable"; 2 | import "core-js/es/set"; 3 | import "core-js/es/map"; 4 | import "regenerator-runtime/runtime"; 5 | import "raf/polyfill"; 6 | import "react-app-polyfill/ie9"; 7 | import "react-app-polyfill/stable"; 8 | 9 | import React from "react"; 10 | import ReactDOM from "react-dom"; 11 | import App from "./App"; 12 | import * as serviceWorker from "./serviceWorker"; 13 | 14 | import createSagaMiddleware from "redux-saga"; 15 | import ReduxThunk from "redux-thunk"; 16 | import { createStore, applyMiddleware } from "redux"; 17 | import { rootReducer, rootSaga } from "./Reducer"; 18 | import { composeWithDevTools } from "redux-devtools-extension"; 19 | import { Provider } from "react-redux"; 20 | import { BrowserRouter } from "react-router-dom"; 21 | 22 | import { ThemeProvider } from "styled-components"; 23 | import theme from "./styles/theme"; 24 | import GlobalStyle from "./styles/GlobalStyle"; 25 | 26 | // import "./index.css"; 27 | 28 | const sagaMiddleware = createSagaMiddleware(); 29 | const store = createStore( 30 | rootReducer, 31 | composeWithDevTools(applyMiddleware(ReduxThunk, sagaMiddleware)) 32 | ); 33 | 34 | sagaMiddleware.run(rootSaga); 35 | 36 | ReactDOM.render( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | , 45 | document.getElementById("root") 46 | ); 47 | 48 | // If you want your app to work offline and load faster, you can change 49 | // unregister() to register() below. Note this comes with some pitfalls. 50 | // Learn more about service workers: https://bit.ly/CRA-PWA 51 | serviceWorker.unregister(); 52 | -------------------------------------------------------------------------------- /src/Components/Main/OrderbookCoinInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const St = { 5 | CandleInfoContainer: styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: flex-end; 9 | width: 33.3333%; 10 | border: 1px solid gray; 11 | margin-top: -1px; 12 | margin-left: -1px; 13 | `, 14 | 15 | InfoContainer: styled.div` 16 | display: flex; 17 | width: 90%; 18 | font-size: 0.8rem; 19 | padding: 5px; 20 | `, 21 | 22 | InfoTxt: styled.span` 23 | display: block; 24 | width: 50%; 25 | `, 26 | 27 | InfoValue: styled.span` 28 | display: block; 29 | width: 50%; 30 | text-align: right; 31 | `, 32 | }; 33 | 34 | const OrderbookCoinInfo = ({ 35 | volume24, 36 | tradePrice24, 37 | highestPrice52Week, 38 | highestDate52Week, 39 | lowestPrice52Week, 40 | lowestDate52Week, 41 | }) => { 42 | return ( 43 | 44 | 45 | 거래량 46 | {volume24} 47 | 48 | 49 | 거래대금 50 | {`${tradePrice24}백만`} 51 | 52 | 53 | 52주 최고 54 | {`${highestPrice52Week} (${highestDate52Week})`} 55 | 56 | 57 | 52주 최저 58 | {`${lowestPrice52Week} (${lowestDate52Week})`} 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default React.memo(OrderbookCoinInfo); 65 | -------------------------------------------------------------------------------- /src/Container/withMarketNames.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import * as Hangul from "hangul-js"; 4 | import { choHangul } from "../Lib/utils"; 5 | 6 | const withMarketNames = () => (OriginalComponent) => (props) => { 7 | const marketNames = useSelector((state) => state.Coin.marketNames.data); // 코인 마켓 이름들(객체) 8 | let marketNamesArr = Object.keys(marketNames); // 코인 마켓 이름 배열화 9 | 10 | const coinListDatas = useSelector((state) => state.Coin.candle.data); // 코인들 데이터 11 | const coinSearchInputData = useSelector((state) => state.Coin.searchCoin); // 검색한 코인 이름 12 | 13 | // 데이터 받는 데 성공하면 필터링 및 정렬한다 14 | if (Object.keys(coinListDatas).length > 1) { 15 | // 검색 기준 필터링 16 | marketNamesArr = marketNamesArr.filter( 17 | (coin) => 18 | // 영어 검색 19 | marketNames[coin].english 20 | .toLowerCase() 21 | .includes(coinSearchInputData.toLowerCase()) || 22 | // 코인 심볼 검색 23 | coin 24 | .split("-")[1] 25 | .toLowerCase() 26 | .includes(coinSearchInputData.toLowerCase()) || 27 | // 한글 검색 28 | Hangul.disassembleToString(marketNames[coin].korean).includes( 29 | Hangul.disassembleToString(coinSearchInputData) 30 | ) || 31 | // 초성 검색 32 | choHangul(marketNames[coin].korean).includes(coinSearchInputData) 33 | ); 34 | 35 | // 정렬 36 | marketNamesArr = marketNamesArr.sort((coin1, coin2) => { 37 | return ( 38 | +coinListDatas[coin2].tradePrice24Hour - 39 | +coinListDatas[coin1].tradePrice24Hour 40 | ); 41 | }); 42 | } 43 | return ( 44 | 49 | ); 50 | }; 51 | 52 | export default withMarketNames; 53 | -------------------------------------------------------------------------------- /src/Container/withOrderbookData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withOrderbookData = () => (OriginalComponent) => (props) => { 5 | const selectedMarket = useSelector((state) => state.Coin.selectedMarket); 6 | const orderbook = useSelector( 7 | (state) => state.Coin.orderbook.data[selectedMarket] 8 | ); 9 | 10 | let totalData; 11 | let bidOrderbookData; 12 | let askOrderbookData; 13 | let orderbookData; 14 | let maxOrderSize = 0; 15 | 16 | if (orderbook) { 17 | totalData = { 18 | totalBidSize: orderbook.total_bid_size, 19 | totalAskSize: orderbook.total_ask_size, 20 | }; 21 | 22 | bidOrderbookData = []; 23 | askOrderbookData = []; 24 | 25 | // let maxOrderSize = 0; 26 | // 호가 데이터 분리 정렬 27 | orderbook.orderbook_units.forEach((orderbook, i) => { 28 | const bidSize = orderbook.bid_size.toFixed(3); 29 | const askSize = orderbook.ask_size.toFixed(3); 30 | 31 | bidOrderbookData.push({ 32 | bidPrice: orderbook.bid_price, 33 | bidSize: orderbook.bid_size.toFixed(3), 34 | }); 35 | askOrderbookData.push({ 36 | askPrice: orderbook.ask_price, 37 | askSize: orderbook.ask_size.toFixed(3), 38 | }); 39 | maxOrderSize = Math.max(maxOrderSize, bidSize, askSize); 40 | }); 41 | 42 | orderbookData = [...askOrderbookData, ...bidOrderbookData]; 43 | // 매도 호가창은 가격 내림차순으로 정렬해줌 (매수는 원래 가격 내림차순임) 44 | askOrderbookData.sort((book1, book2) => +book2.askPrice - +book1.askPrice); 45 | } 46 | 47 | return ( 48 | 56 | ); 57 | }; 58 | 59 | export default withOrderbookData; 60 | -------------------------------------------------------------------------------- /src/Api/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const coinApi = { 4 | getMarketCodes: () => 5 | axios.get("https://api.upbit.com/v1/market/all?isDetails=false"), 6 | getInitCanldes: (coins) => 7 | axios.get(`https://api.upbit.com/v1/ticker?markets=${coins}`), 8 | getInitOrderbooks: (coins) => 9 | axios.get(`https://api.upbit.com/v1/orderbook?markets=${coins}`), 10 | getOneCoinCandles: ({ coin, timeType, timeCount }) => { 11 | if (timeType === "minutes") 12 | return axios 13 | .get( 14 | `https://api.upbit.com/v1/candles/${timeType}/${timeCount}?market=${coin}&count=200` 15 | ) 16 | .then((res) => { 17 | return { 18 | ...res, 19 | data: res.data.sort((a, b) => a.timestamp - b.timestamp), 20 | }; 21 | }); 22 | else 23 | return axios 24 | .get( 25 | `https://api.upbit.com/v1/candles/${timeType}?market=${coin}&count=200` 26 | ) 27 | .then((res) => { 28 | return { 29 | ...res, 30 | data: res.data.sort((a, b) => a.timestamp - b.timestamp), 31 | }; 32 | }); 33 | }, 34 | getAdditionalCoinCandles: ({ coin, timeType, timeCount, datetime }) => { 35 | if (timeType === "minutes") 36 | return axios 37 | .get( 38 | `https://api.upbit.com/v1/candles/${timeType}/${timeCount}?market=${coin}&to=${datetime}&count=200` 39 | ) 40 | .then((res) => { 41 | return { 42 | ...res, 43 | data: res.data.sort((a, b) => a.timestamp - b.timestamp), 44 | }; 45 | }); 46 | else 47 | return axios 48 | .get( 49 | `https://api.upbit.com/v1/candles/${timeType}?market=${coin}&to=${datetime}&count=200` 50 | ) 51 | .then((res) => { 52 | return { 53 | ...res, 54 | data: res.data.sort((a, b) => a.timestamp - b.timestamp), 55 | }; 56 | }); 57 | }, 58 | getOneCoinTradeLists: (coin) => 59 | axios.get(`https://api.upbit.com/v1/trades/ticks?market=${coin}&count=50`), 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upbit-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/polyfill": "^7.12.1", 7 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 8 | "@fortawesome/free-brands-svg-icons": "^5.15.1", 9 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 10 | "@fortawesome/react-fontawesome": "^0.1.12", 11 | "@testing-library/jest-dom": "^4.2.4", 12 | "@testing-library/react": "^9.3.2", 13 | "@testing-library/user-event": "^7.1.2", 14 | "axios": "^0.20.0", 15 | "core-js": "^3.8.3", 16 | "d3": "^5.15.1", 17 | "d3-selection": "^1.1.0", 18 | "d3-transition": "^1.3.2", 19 | "d3fc": "^15.0.16", 20 | "decimal.js": "^10.2.1", 21 | "hangul-js": "^0.2.6", 22 | "install": "^0.13.0", 23 | "lodash": "^4.17.20", 24 | "moment-timezone": "^0.5.31", 25 | "npm": "^6.14.9", 26 | "raf": "^3.4.1", 27 | "react": "^16.13.1", 28 | "react-app-polyfill": "^2.0.0", 29 | "react-dom": "^16.13.1", 30 | "react-fast-compare": "^3.2.0", 31 | "react-financial-charts": "1.0.0-alpha.16", 32 | "react-loading": "^2.0.3", 33 | "react-redux": "^7.2.1", 34 | "react-router-dom": "^5.2.0", 35 | "react-scripts": "3.2.0", 36 | "redux": "^4.0.5", 37 | "redux-devtools-extension": "^2.13.8", 38 | "redux-saga": "^1.1.3", 39 | "redux-thunk": "^2.3.0", 40 | "regenerator-runtime": "^0.13.7", 41 | "styled-components": "^5.2.0", 42 | "styled-normalize": "^8.0.7", 43 | "styled-reset": "^4.3.0", 44 | "text-encoding": "^0.7.0", 45 | "websocket": "^1.0.32" 46 | }, 47 | "scripts": { 48 | "start": "react-scripts start", 49 | "build": "react-scripts build", 50 | "test": "react-scripts test", 51 | "eject": "react-scripts eject" 52 | }, 53 | "eslintConfig": { 54 | "extends": "react-app" 55 | }, 56 | "browserslist": { 57 | "production": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "development": [ 63 | ">0.2%", 64 | "ie 9", 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version", 68 | "not dead" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 106 | 107 | # dependencies 108 | /node_modules 109 | /.pnp 110 | .pnp.js 111 | 112 | # testing 113 | /coverage 114 | 115 | # production 116 | /build 117 | 118 | # misc 119 | .DS_Store 120 | .env.local 121 | .env.development.local 122 | .env.test.local 123 | .env.production.local 124 | 125 | npm-debug.log* 126 | yarn-debug.log* 127 | yarn-error.log* 128 | -------------------------------------------------------------------------------- /src/Components/Main/MainChart-d3fc.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import * as d3 from "d3"; 3 | import * as fc from "d3fc"; 4 | import { useSelector } from "react-redux"; 5 | 6 | const MainChart = () => { 7 | const selectedMarket = useSelector((state) => state.Coin.selectedMarket); 8 | // const selectedCandles = useSelector( 9 | // (state) => state.Coin.candle.data[selectedMarket].candles 10 | // ); 11 | // console.log(test[0].date); 12 | 13 | const selectedCandles = fc.randomFinancial()(200); 14 | let updated = false; 15 | 16 | if (selectedCandles.length > 2) { 17 | let updated = true; 18 | 19 | // console.log("여기야", selectedCandles); 20 | const xExtent = fc.extentDate().accessors([(d) => d.date]); 21 | const yExtent = fc.extentLinear().accessors([(d) => d.high, (d) => d.low]); 22 | 23 | const gridlines = fc.annotationSvgGridline(); 24 | 25 | const candlestick = fc.seriesSvgCandlestick().decorate((sel) => { 26 | sel.enter().style("fill", (_, i) => (_.open > _.close ? "blue" : "red")); 27 | sel 28 | .enter() 29 | .style("stroke", (_, i) => (_.open > _.close ? "blue" : "red")); 30 | }); 31 | 32 | const multi = fc.seriesSvgMulti().series([gridlines, candlestick]); 33 | // 줌용 설정 34 | const x = d3.scaleTime(); 35 | const y = d3.scaleLinear(); 36 | 37 | const x2 = d3.scaleTime(); 38 | const y2 = d3.scaleLinear(); 39 | 40 | const zoom = d3.zoom().on("zoom", () => { 41 | // update the scale used by the chart to use the updated domain 42 | 43 | x.domain(d3.event.transform.rescaleX(x2).domain()); 44 | y.domain(d3.event.transform.rescaleY(y2).domain()); 45 | d3.select(".chartContainer").datum(selectedCandles).call(chart); 46 | }); 47 | const chart = fc 48 | .chartCartesian(x, y) 49 | .xDomain(xExtent(selectedCandles)) 50 | .yDomain(yExtent(selectedCandles)) 51 | .svgPlotArea(multi) 52 | .decorate((sel) => { 53 | sel 54 | .enter() 55 | .select(".plot-area") 56 | .on("measure.range", () => { 57 | x2.range([0, d3.event.detail.width]); 58 | y2.range([d3.event.detail.height, 0]); 59 | }) 60 | .call(zoom); 61 | }); 62 | 63 | x2.domain(chart.xDomain()); 64 | y2.domain(chart.yDomain()); 65 | 66 | d3.select(".chartContainer").datum(selectedCandles).call(chart); 67 | } 68 | 69 | useEffect(() => {}, [selectedCandles]); 70 | 71 | return ( 72 |
73 | {/* */} 74 |
75 | ); 76 | }; 77 | 78 | export default MainChart; 79 | -------------------------------------------------------------------------------- /src/Container/withSelectedCoinPrice.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const withSelectedCoinPrice = () => (OriginalComponent) => (props) => { 5 | const selectedMarket = useSelector((state) => state.Coin.selectedMarket); 6 | const selectedCoinData = useSelector( 7 | (state) => state.Coin.candle.data[selectedMarket] 8 | ); 9 | 10 | // 24시간 고가 저가 11 | const highestPrice24Hour = useSelector( 12 | (state) => state.Coin.candle.data[selectedMarket]["highestPrice24Hour"] 13 | ); 14 | const lowestPrice24Hour = useSelector( 15 | (state) => state.Coin.candle.data[selectedMarket]["lowestPrice24Hour"] 16 | ); 17 | 18 | // 52주 고가 저가 19 | const highestPrice52Week = useSelector( 20 | (state) => state.Coin.candle.data[selectedMarket].highestPrice52Week 21 | ); 22 | const highestDate52Week = useSelector( 23 | (state) => state.Coin.candle.data[selectedMarket].highestDate52Week 24 | ); 25 | const lowestPrice52Week = useSelector( 26 | (state) => state.Coin.candle.data[selectedMarket].lowestPrice52Week 27 | ); 28 | const lowestDate52Week = useSelector( 29 | (state) => state.Coin.candle.data[selectedMarket].lowestDate52Week 30 | ); 31 | 32 | // 24시간 거래대금, 거래량 33 | const tradePrice24Hour = Math.floor(selectedCoinData.tradePrice24Hour); 34 | const volume24Hour = Math.floor(selectedCoinData.volume24Hour); 35 | 36 | // 24시간 가격 변화율, 변화량 37 | const changeRate24Hour = 38 | Math.round(selectedCoinData.changeRate24Hour * 10000) / 100; 39 | const changePrice24Hour = selectedCoinData.changePrice24Hour 40 | ? selectedCoinData.changePrice24Hour 41 | : 0; 42 | 43 | // 전일, 당일 가격 44 | const selecteCoinCadnles = useSelector( 45 | (state) => state.Coin.candle.data[selectedMarket].candles 46 | ); 47 | const lastCandleIndex = selecteCoinCadnles.length - 1; 48 | 49 | const beforeDayPrice = selecteCoinCadnles.length 50 | ? selecteCoinCadnles[lastCandleIndex].close - changePrice24Hour 51 | : 0; 52 | 53 | const price = selecteCoinCadnles.length 54 | ? selecteCoinCadnles[selecteCoinCadnles.length - 1].close 55 | : 0; 56 | 57 | return ( 58 | 73 | ); 74 | }; 75 | 76 | export default withSelectedCoinPrice; 77 | -------------------------------------------------------------------------------- /src/Components/Main/TradeListItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import isEqual from "react-fast-compare"; 4 | 5 | const St = { 6 | TradeListLi: styled.li` 7 | display: flex; 8 | justify-content: space-around; 9 | align-items: center; 10 | width: 100%; 11 | height: 25px; 12 | font-size: 0.9em; 13 | background-color: ${({ bgColor }) => bgColor || "white"}; 14 | `, 15 | 16 | Datetime: styled.div` 17 | width: 20%; 18 | text-align: center; 19 | @media ${({ theme }) => theme.mobileS} { 20 | display: none; 21 | } 22 | `, 23 | Date: styled.span` 24 | text-align: center; 25 | `, 26 | 27 | Time: styled.span` 28 | text-align: center; 29 | font-size: 0.8rem; 30 | margin-left: 5px; 31 | `, 32 | 33 | TradePrice: styled.span` 34 | display: block; 35 | width: 20%; 36 | text-align: center; 37 | color: ${({ fontColor }) => fontColor}; 38 | font-weight: 600; 39 | 40 | @media ${({ theme }) => theme.mobileS} { 41 | width: 50%; 42 | font-size: 0.7rem; 43 | } 44 | @media ${({ theme }) => theme.mobileM} { 45 | /* width: 50%; */ 46 | font-size: 0.8rem; 47 | } 48 | `, 49 | 50 | TradeAmount: styled.span` 51 | display: block; 52 | width: 20%; 53 | text-align: center; 54 | color: ${({ fontColor }) => fontColor}; 55 | 56 | @media ${({ theme }) => theme.mobileS} { 57 | width: 50%; 58 | font-size: 0.7rem; 59 | } 60 | 61 | @media ${({ theme }) => theme.mobileM} { 62 | /* width: 50%; */ 63 | font-size: 0.8rem; 64 | } 65 | `, 66 | 67 | TradeKRW: styled.span` 68 | display: block; 69 | width: 20%; 70 | text-align: right; 71 | 72 | @media ${({ theme }) => theme.mobileS} { 73 | display: none; 74 | } 75 | 76 | @media ${({ theme }) => theme.mobileM} { 77 | display: none; 78 | } 79 | `, 80 | }; 81 | 82 | const TradeListItem = ({ 83 | theme, 84 | index, 85 | date, 86 | time, 87 | tradePrice, 88 | changePrice, 89 | tradeAmount, 90 | askBid, 91 | }) => { 92 | return ( 93 | 97 | 98 | {date} 99 | {time} 100 | 101 | 0 ? theme.priceUp : theme.priceDown} 103 | > 104 | {tradePrice.toLocaleString()} 105 | 106 | 110 | {tradeAmount.toFixed(5)} 111 | 112 | 113 | {Math.floor(tradePrice * tradeAmount).toLocaleString()} 114 | 115 | 116 | ); 117 | }; 118 | 119 | export default React.memo(TradeListItem, isEqual); 120 | -------------------------------------------------------------------------------- /src/Components/Main/OrderInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import styled from "styled-components"; 4 | import OrderInfoAskBid from "./OrderInfoAskBid"; 5 | 6 | import withSelectedOption from "../../Container/withSelectedOption"; 7 | import withThemeData from "../../Container/withThemeData"; 8 | import withSelectedCoinName from "../../Container/withSelectedCoinName"; 9 | 10 | import { changeAskBidOrder } from "../../Reducer/coinReducer"; 11 | import isEqual from "react-fast-compare"; 12 | 13 | const St = { 14 | Container: styled.section` 15 | width: 100%; 16 | height: 50%; 17 | background-color: white; 18 | `, 19 | HiddenH3: styled.h3` 20 | position: absolute; 21 | width: 1px; 22 | height: 1px; 23 | clip: rect(0, 0); 24 | clip-path: polygon(0, 0); 25 | overflow: hidden; 26 | text-indent: -9999px; 27 | `, 28 | OrderTypeContainer: styled.ul` 29 | display: flex; 30 | height: 40px; 31 | align-items: center; 32 | border-bottom: 1px solid ${({ theme }) => theme.lightGray2}; 33 | 34 | @media ${({ theme }) => theme.mobileS} { 35 | font-size: 0.8rem; 36 | } 37 | `, 38 | OrderTypeLi: styled.li` 39 | width: 33.3333%; 40 | height: 100%; 41 | `, 42 | OrderTypeBtn: styled.button` 43 | width: 100%; 44 | height: 100%; 45 | background-color: white; 46 | border: none; 47 | border-bottom: 3px solid ${({ borderBottom }) => borderBottom || "white"}; 48 | outline: 0; 49 | font-weight: 900; 50 | color: ${({ fontColor }) => fontColor || "black"}; 51 | cursor: pointer; 52 | `, 53 | }; 54 | 55 | const OrderInfo = ({ 56 | theme, 57 | selectedAskBidOrder, 58 | coinSymbol, 59 | orderPrice, 60 | orderAmount, 61 | orderTotalPrice, 62 | }) => { 63 | const dispatch = useDispatch(); 64 | return ( 65 | 66 | 주문 정보 67 | 68 | 69 | dispatch(changeAskBidOrder("bid"))} 73 | > 74 | 매수 75 | 76 | 77 | 78 | dispatch(changeAskBidOrder("ask"))} 82 | > 83 | 매도 84 | 85 | 86 | 87 | dispatch(changeAskBidOrder("tradeList"))} 91 | > 92 | 거래내역 93 | 94 | 95 | 96 | 104 | 105 | ); 106 | }; 107 | 108 | export default withSelectedCoinName()( 109 | withSelectedOption()(withThemeData()(React.memo(OrderInfo, isEqual))) 110 | ); 111 | -------------------------------------------------------------------------------- /src/Components/Main/ChartDataConsole.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import styled from "styled-components"; 4 | import withSelectedOption from "../../Container/withSelectedOption"; 5 | import withThemeData from "../../Container/withThemeData"; 6 | import { changeTimeTypeAndData } from "../../Reducer/coinReducer"; 7 | 8 | const St = { 9 | Container: styled.div` 10 | display: flex; 11 | width: 100%; 12 | background-color: white; 13 | border-bottom: 1px solid ${({ theme }) => theme.lightGray2}; 14 | `, 15 | HiddenH3: styled.h3` 16 | position: absolute; 17 | width: 1px; 18 | height: 1px; 19 | clip: rect(0, 0); 20 | clip-path: polygon(0, 0); 21 | overflow: hidden; 22 | text-indent: -9999px; 23 | `, 24 | TimeBtnContainer: styled.div` 25 | display: flex; 26 | align-items: center; 27 | height: 30px; 28 | `, 29 | TimeBtn: styled.button` 30 | /* width: 50px; */ 31 | height: 20px; 32 | width: 38px; 33 | margin-left: 5px; 34 | font-size: 0.8rem; 35 | background-color: white; 36 | 37 | border: ${({ theme, isSelected }) => 38 | isSelected ? `2px solid black` : `1px solid ${theme.lightGray2}`}; 39 | outline: none; 40 | cursor: pointer; 41 | `, 42 | }; 43 | 44 | const ChartDataConsole = ({ theme, selectedTimeCount, selectedTimeType }) => { 45 | const dispatch = useDispatch(); 46 | 47 | const changeChartTime = useCallback( 48 | (timeCount, timeType) => () => { 49 | dispatch(changeTimeTypeAndData({ timeCount, timeType })); 50 | }, 51 | [dispatch] 52 | ); 53 | 54 | return ( 55 | 56 | 차트에 표시할 캔들의 시간 선택 57 | 58 | 62 | 1m 63 | 64 | 68 | 3m 69 | 70 | 74 | 5m 75 | 76 | 82 | 10m 83 | 84 | 90 | 15m 91 | 92 | 98 | 1h 99 | 100 | 106 | 4h 107 | 108 | 112 | 1d 113 | 114 | 115 | 116 | ); 117 | }; 118 | 119 | export default withSelectedOption()(withThemeData()(ChartDataConsole)); 120 | -------------------------------------------------------------------------------- /src/Components/Global/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { faEnvelope } from "@fortawesome/free-solid-svg-icons"; 4 | import { faGithub } from "@fortawesome/free-brands-svg-icons"; 5 | 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | 8 | const St = { 9 | Footer: styled.footer` 10 | display: block; 11 | width: 100%; 12 | height: 100%; 13 | /* height: 120px; */ 14 | background-color: white; 15 | padding: 20px 0; 16 | @media ${({ theme }) => theme.tablet} { 17 | display: none; 18 | } 19 | `, 20 | Container: styled.div` 21 | display: flex; 22 | justify-content: space-between; 23 | align-items: center; 24 | width: 100%; 25 | height: 100%; 26 | max-width: 1360px; 27 | margin: 0 auto; 28 | padding: 0 20px; 29 | 30 | @media ${({ theme }) => theme.tablet} { 31 | display: block; 32 | max-width: 950px; 33 | } 34 | `, 35 | MainLink: styled.a` 36 | display: block; 37 | background-image: ${({ logo }) => `url(${logo})`}; 38 | background-repeat: no-repeat; 39 | background-position: center; 40 | background-size: contain; 41 | color: transparent; 42 | width: 130px; 43 | height: 60px; 44 | `, 45 | Description: styled.p` 46 | font-weight: 600; 47 | font-size: 0.9rem; 48 | color: gray; 49 | height: 85px; 50 | margin-top: 10px; 51 | /* margin-left: 250px; */ 52 | `, 53 | DescSpan: styled.span` 54 | display: block; 55 | height: 30px; 56 | `, 57 | ContactContainer: styled.address` 58 | display: flex; 59 | flex-direction: column; 60 | /* margin-left: 250px; */ 61 | `, 62 | LinkTitle: styled.span` 63 | height: 25px; 64 | font-size: 0.9rem; 65 | font-weight: 600; 66 | color: gray; 67 | `, 68 | LinkTag: styled.a` 69 | display: flex; 70 | align-items: center; 71 | height: 30px; 72 | color: black; 73 | text-decoration: none; 74 | `, 75 | LinkSpan: styled.span` 76 | display: block; 77 | margin-left: ${({ marginLeft }) => marginLeft || "8px"}; 78 | font-weight: 600; 79 | font-size: 0.9rem; 80 | height: 20px; 81 | /* line-height: 1.5rem; */ 82 | color: gray; 83 | `, 84 | }; 85 | 86 | const Footer = () => { 87 | return ( 88 | 89 | 90 | 95 | 96 | Upbit Clone Project - Downbit 97 | Created by Seongkyun Yu 98 | 99 | Copyright © 2020 DOWNBIT INC. ALL RIGHTS RESERVED. 100 | 101 | 102 | 103 | Contact Me 104 |
    105 |
  • 106 | 107 | 112 | github.com/Seongkyun-Yu/upbit-clone 113 | 114 |
  • 115 |
  • 116 | 117 | 122 | ysungkyun@gmail.com 123 | 124 |
  • 125 |
126 |
127 |
128 |
129 | ); 130 | }; 131 | 132 | export default Footer; 133 | -------------------------------------------------------------------------------- /src/Pages/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import withSize from "../Container/withSize"; 4 | import { viewSize } from "../styles/theme"; 5 | 6 | import Header from "../Components/Global/Header"; 7 | import CoinInfoHeader from "../Components/Main/CoinInfoHeader"; 8 | import ChartDataConsole from "../Components/Main/ChartDataConsole"; 9 | import MainChart from "../Components/Main/MainChart"; 10 | import Orderbook from "../Components/Main/Orderbook"; 11 | import OrderInfo from "../Components/Main/OrderInfo"; 12 | import TradeList from "../Components/Main/TradeList"; 13 | import CoinList from "../Components/Main/CoinList"; 14 | import Footer from "../Components/Global/Footer"; 15 | 16 | const St = { 17 | MainContentContainer: styled.div` 18 | display: flex; 19 | justify-content: center; 20 | max-width: 1500px; 21 | margin: 0 auto; 22 | margin-top: 10px; 23 | margin-bottom: 50px; 24 | width: 100%; 25 | height: 100%; 26 | 27 | @media ${({ theme }) => theme.tablet} { 28 | margin-top: 0; 29 | margin-bottom: 0; 30 | } 31 | `, 32 | ChartAndTradeContainer: styled.section` 33 | display: flex; 34 | flex-direction: column; 35 | align-items: center; 36 | width: 95%; 37 | max-width: 950px; 38 | 39 | @media ${(props) => (props.isRootURL ? props.theme.tablet : true)} { 40 | display: none; 41 | } 42 | `, 43 | HiddenH2: styled.h2` 44 | position: absolute; 45 | width: 1px; 46 | height: 1px; 47 | clip: rect(0, 0); 48 | clip-path: polygon(0, 0); 49 | overflow: hidden; 50 | text-indent: -9999px; 51 | `, 52 | MainChartContainer: styled.div` 53 | width: 100%; 54 | height: 500; 55 | `, 56 | TradeInfoContainer: styled.div` 57 | display: flex; 58 | width: 100%; 59 | margin-top: 10px; 60 | @media ${({ theme }) => theme.mobileM} { 61 | margin-top: 0; 62 | } 63 | `, 64 | TradeOrderContainer: styled.div` 65 | display: flex; 66 | flex-direction: column; 67 | width: 55%; 68 | min-width: 180px; 69 | margin-left: 10px; 70 | @media ${({ theme }) => theme.mobileM} { 71 | margin-left: 0; 72 | border: 2px solid ${({ theme }) => theme.lightGray1}; 73 | /* border-top: 1px solid ${({ theme }) => theme.lightGray1}; 74 | border-bottom: 1px solid ${({ theme }) => theme.lightGray1}; 75 | border-left: 1px solid ${({ theme }) => theme.lightGray1}; */ 76 | } 77 | `, 78 | }; 79 | 80 | const Main = ({ match, widthSize, heightSize }) => { 81 | const isRootURL = match.path === "/"; 82 | 83 | return ( 84 | <> 85 |
86 | 87 | { 88 | // 차트 및 주문 관련 뷰는 메인 페이지이면서 tablet 사이즈보다 크거나, 메인 페이지가 아닌 경우에만 그린다 89 | ((isRootURL && widthSize > viewSize.tablet) || !isRootURL) && ( 90 | 91 | 차트 및 주문 정보 창 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ) 104 | } 105 | { 106 | // 코인 리스트 뷰는 메인 페이지이거나, 메인 페이지가 아니면서 tablet 사이즈보다 큰 경우에만 그린다 107 | (isRootURL || (!isRootURL && widthSize > viewSize.tablet)) && ( 108 | 113 | ) 114 | } 115 | 116 |