commit
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Seongkyun Yu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
455
README.md
Normal file
455
README.md
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
# Downbit 프로젝트 
|
||||||
|
|
||||||
|
| [](https://youtu.be/zsuSvv9IfM8) |
|
||||||
|
| :----------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||||
|
| _이미지 클릭시 YouTube로 연결됩니다_ |
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
2020년 9월 1일부터 11월 10일 동안 매일 2시간씩 진행한 업비트 클론 프로젝트 입니다.<br>
|
||||||
|
|
||||||
|
~~[downbit.ml](https://downbit.ml)에서 배포된 프로젝트 내역을 확인하실 수 있습니다.~~<br>
|
||||||
|
두나무의 요청으로 배포를 중단했습니다.<br>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Development motivation
|
||||||
|
|
||||||
|
Upbit의 실제 거래 데이터를 통해<br>
|
||||||
|
|
||||||
|
많은 데이터 수신시 프론트 엔드의 뷰를 최적화 하는 방법을 학습하고자<br>
|
||||||
|
|
||||||
|
이번 프로젝트를 시작하였습니다.<br>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Skill
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
<br>
|
||||||
|

|
||||||
|

|
||||||
|
<br>
|
||||||
|

|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Library
|
||||||
|
<details>
|
||||||
|
<summary>접기/펼치기 버튼</summary>
|
||||||
|
<div markdown="1">
|
||||||
|
React v.16<br>
|
||||||
|
axios: 0.20.0<br>
|
||||||
|
d3: 5.15.1<br>
|
||||||
|
react-redux: 7.2.1<br>
|
||||||
|
redux-saga v.1.1.3<br>
|
||||||
|
redux-thunk v.2.3.0<br>
|
||||||
|
react-router-dom v.5.2.0<br>
|
||||||
|
axios v.0.19.2<br>
|
||||||
|
websocket: 1.0.32<br>
|
||||||
|
react-fast-compare: 3.2.0<br>
|
||||||
|
react-financial-charts: 1.0.0-alpha.16<br>
|
||||||
|
decimal.js: 10.2.1<br>
|
||||||
|
hangul-js: 0.2.6<br>
|
||||||
|
lodash: 4.17.20<br>
|
||||||
|
moment-timezone: 0.5.31<br>
|
||||||
|
styled-components: 5.2.0<br>
|
||||||
|
styled-normalize: 8.0.7<br>
|
||||||
|
styled-reset": 4.3.0<br>
|
||||||
|
@fortawesome/free-brands-svg-icons: 5.15.1<br>
|
||||||
|
@fortawesome/free-solid-svg-icons: 5.15.1<br>
|
||||||
|
@fortawesome/react-fontawesome: 0.1.12<br>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
$ git clone https://github.com/Seongkyun-Yu/upbit-clone.git<br>
|
||||||
|
$ yarn install<br>
|
||||||
|
\$ yarn start<br>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Main Feature (프로젝트의 모든 기능을 혼자 개발했습니다)
|
||||||
|
|
||||||
|
- 실시간 가격, 거래량 등의 데이터 수신 및 차트 랜더링
|
||||||
|
- 실시간 호가창, 거래내역 랜더링
|
||||||
|
- 코인 초성, 심볼 검색
|
||||||
|
- 매수 총액에 따른 구매수량 자동 조절, 가격 변경에 따른 구매 총액 자동 변경
|
||||||
|
- 호가창 클릭시 자동 가격 입력
|
||||||
|
- 반응형
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 프로젝트 구조
|
||||||
|
|
||||||
|
```bash
|
||||||
|
├── node_modules
|
||||||
|
├── public
|
||||||
|
│ ├── blueLogo.png
|
||||||
|
│ ├── whiteLogo.png
|
||||||
|
│ ├── favicon.png
|
||||||
|
│ └── index.html
|
||||||
|
├── build
|
||||||
|
├── src
|
||||||
|
│ ├── Api
|
||||||
|
│ │ └── api.js
|
||||||
|
│ ├── Components
|
||||||
|
│ │ ├── Global
|
||||||
|
│ │ │ ├── Header.js
|
||||||
|
│ │ │ ├── Footer.js
|
||||||
|
│ │ │ └── Loading.js
|
||||||
|
│ │ └── Main
|
||||||
|
│ │ ├── ChartDataConsole.js
|
||||||
|
│ │ ├── CoinInfoHeader.js
|
||||||
|
│ │ ├── CoinList.js
|
||||||
|
│ │ ├── CoinListItem.js
|
||||||
|
│ │ ├── MainChart.js
|
||||||
|
│ │ ├── Orderbook.js
|
||||||
|
│ │ ├── OrderbookCoinInfo.js
|
||||||
|
│ │ ├── OrderbookItem.js
|
||||||
|
│ │ ├── OrderInfo.js
|
||||||
|
│ │ ├── OrderInfoAskBid.js
|
||||||
|
│ │ ├── OrderInfoTradeList.js
|
||||||
|
│ │ ├── TradeList.js
|
||||||
|
│ │ └── TradeListItem.js
|
||||||
|
│ ├── Pages
|
||||||
|
│ │ └── Main.js
|
||||||
|
│ ├── Container <-- HOC
|
||||||
|
│ │ ├── withLatestCoinData.js
|
||||||
|
│ │ ├── withLoadingData.js
|
||||||
|
│ │ ├── withMarketNames.js
|
||||||
|
│ │ ├── withOHLCData.js
|
||||||
|
│ │ └── ...etc
|
||||||
|
│ ├── Lib
|
||||||
|
│ │ ├── asyncUtil.js <-- redux-saga, thunk factory pattern
|
||||||
|
│ │ └── utils.js <-- etc utils
|
||||||
|
│ ├── Reducer
|
||||||
|
│ │ ├── index.js
|
||||||
|
│ │ ├── coinReducer.js
|
||||||
|
│ │ └── loadingReducer.js
|
||||||
|
│ ├── Router
|
||||||
|
│ │ └── MainRouter.js
|
||||||
|
│ ├── styles
|
||||||
|
│ │ ├── fonts
|
||||||
|
│ │ ├── GlobalStyle.js
|
||||||
|
│ │ └── theme.js
|
||||||
|
│ ├── App.js
|
||||||
|
│ └── index.js
|
||||||
|
├── README.md
|
||||||
|
├── LICENSE
|
||||||
|
├── package.json
|
||||||
|
├── yarn.lock
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 프로젝트 관련 생각들
|
||||||
|
|
||||||
|
|
||||||
|
- [buffer를 활용하여 상태 갱신 줄이기](https://velog.io/@seongkyun/React-%EC%B5%9C%EC%A0%81%ED%99%94-buffer%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%83%81%ED%83%9C-%EA%B0%B1%EC%8B%A0-%EC%A4%84%EC%9D%B4%EA%B8%B0)
|
||||||
|
- [throttle로 이벤트 캐치 줄이기](https://velog.io/@seongkyun/React-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%98%EC%9D%91%ED%98%95%EA%B3%BC-display-none)
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Technical Issue: Optimization
|
||||||
|
|
||||||
|
- 1초에 최대 150개의 데이터가 전송되어 상태를 변경시킴
|
||||||
|
<!-- - <details>
|
||||||
|
<summary>해결 코드 접기/펼치기 버튼</summary>
|
||||||
|
<div markdown="1">
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { call, put, select, flush, delay } from "redux-saga/effects";
|
||||||
|
import { buffers, eventChannel } from "redux-saga";
|
||||||
|
|
||||||
|
// 소켓 만들기
|
||||||
|
const createSocket = () => {
|
||||||
|
const client = new W3CWebSocket("wss://api.upbit.com/websocket/v1");
|
||||||
|
client.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 소켓 연결용
|
||||||
|
const connectSocekt = (socket, connectType, action, buffer) => {
|
||||||
|
return eventChannel((emit) => {
|
||||||
|
socket.onopen = () => {
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify([
|
||||||
|
{ ticket: "downbit-clone" },
|
||||||
|
{ type: connectType, codes: action.payload },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (evt) => {
|
||||||
|
const enc = new TextDecoder("utf-8");
|
||||||
|
const arr = new Uint8Array(evt.data);
|
||||||
|
const data = JSON.parse(enc.decode(arr));
|
||||||
|
|
||||||
|
emit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (evt) => {
|
||||||
|
emit(evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = () => {
|
||||||
|
socket.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, buffer || buffers.none());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 웹소켓 연결용 사가
|
||||||
|
const createConnectSocketSaga = (type, connectType, dataMaker) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
const ERROR = `${type}_ERROR`;
|
||||||
|
|
||||||
|
return function* (action = {}) {
|
||||||
|
const client = yield call(createSocket);
|
||||||
|
const clientChannel = yield call(
|
||||||
|
connectSocekt,
|
||||||
|
client,
|
||||||
|
connectType,
|
||||||
|
action,
|
||||||
|
buffers.expanding(500)
|
||||||
|
);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const datas = yield flush(clientChannel); // 버퍼 데이터 가져오기
|
||||||
|
const state = yield select();
|
||||||
|
|
||||||
|
if (datas.length) {
|
||||||
|
const sortedObj = {};
|
||||||
|
datas.forEach((data) => {
|
||||||
|
if (sortedObj[data.code]) {
|
||||||
|
// 버퍼에 있는 데이터중 시간이 가장 최근인 데이터만 남김
|
||||||
|
sortedObj[data.code] =
|
||||||
|
sortedObj[data.code].timestamp > data.timestamp
|
||||||
|
? sortedObj[data.code]
|
||||||
|
: data;
|
||||||
|
} else {
|
||||||
|
sortedObj[data.code] = data; // 새로운 데이터면 그냥 넣음
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedData = Object.keys(sortedObj).map(
|
||||||
|
(data) => sortedObj[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
yield put({
|
||||||
|
type: SUCCESS,
|
||||||
|
payload: dataMaker(sortedData, state),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
yield delay(500); // 500ms 동안 대기
|
||||||
|
} catch (e) {
|
||||||
|
yield put({ type: ERROR, payload: e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details> -->
|
||||||
|
|
||||||
|
- Push 방식의 WebSocket을 Redux-Saga를 이용하여 Pull 방식으로 변경
|
||||||
|
- Redux-Saga의 eventChannel을 이용하여 버퍼 생성
|
||||||
|
- 0.5초에 한 번 버퍼를 확인하여 중복된 데이터 제거 후 변경내역을 상태에 한번에 업데이트
|
||||||
|
|
||||||
|
- ```javascript
|
||||||
|
import { call, put, select, flush, delay } from "redux-saga/effects";
|
||||||
|
import { buffers, eventChannel } from "redux-saga";
|
||||||
|
|
||||||
|
// 소켓 만들기
|
||||||
|
const createSocket = () => {
|
||||||
|
const client = new W3CWebSocket("wss://api.upbit.com/websocket/v1");
|
||||||
|
client.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 소켓 연결용
|
||||||
|
const connectSocekt = (socket, connectType, action, buffer) => {
|
||||||
|
return eventChannel((emit) => {
|
||||||
|
socket.onopen = () => {
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify([
|
||||||
|
{ ticket: "downbit-clone" },
|
||||||
|
{ type: connectType, codes: action.payload },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (evt) => {
|
||||||
|
const enc = new TextDecoder("utf-8");
|
||||||
|
const arr = new Uint8Array(evt.data);
|
||||||
|
const data = JSON.parse(enc.decode(arr));
|
||||||
|
|
||||||
|
emit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (evt) => {
|
||||||
|
emit(evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = () => {
|
||||||
|
socket.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, buffer || buffers.none());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 웹소켓 연결용 사가
|
||||||
|
const createConnectSocketSaga = (type, connectType, dataMaker) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
const ERROR = `${type}_ERROR`;
|
||||||
|
|
||||||
|
return function* (action = {}) {
|
||||||
|
const client = yield call(createSocket);
|
||||||
|
const clientChannel = yield call(
|
||||||
|
connectSocekt,
|
||||||
|
client,
|
||||||
|
connectType,
|
||||||
|
action,
|
||||||
|
buffers.expanding(500)
|
||||||
|
);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const datas = yield flush(clientChannel); // 버퍼 데이터 가져오기
|
||||||
|
const state = yield select();
|
||||||
|
|
||||||
|
if (datas.length) {
|
||||||
|
const sortedObj = {};
|
||||||
|
datas.forEach((data) => {
|
||||||
|
if (sortedObj[data.code]) {
|
||||||
|
// 버퍼에 있는 데이터중 시간이 가장 최근인 데이터만 남김
|
||||||
|
sortedObj[data.code] =
|
||||||
|
sortedObj[data.code].timestamp > data.timestamp
|
||||||
|
? sortedObj[data.code]
|
||||||
|
: data;
|
||||||
|
} else {
|
||||||
|
sortedObj[data.code] = data; // 새로운 데이터면 그냥 넣음
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedData = Object.keys(sortedObj).map(
|
||||||
|
(data) => sortedObj[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
yield put({
|
||||||
|
type: SUCCESS,
|
||||||
|
payload: dataMaker(sortedData, state),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
yield delay(500); // 500ms 동안 대기
|
||||||
|
} catch (e) {
|
||||||
|
yield put({ type: ERROR, payload: e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- 반응형으로 제작시 보이지 않는 컴포넌트를 랜더링 처리
|
||||||
|
|
||||||
|
<!-- - <details>
|
||||||
|
<summary>해결 코드 접기/펼치기 버튼</summary>
|
||||||
|
<div markdown="1">
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
|
||||||
|
const withSize = () => (OriginalComponent) => (props) => {
|
||||||
|
const [widthSize, setWidthSize] = useState(window.innerWidth);
|
||||||
|
const [heightSize, setHeightSize] = useState(window.innerHeight);
|
||||||
|
|
||||||
|
const handleSize = useCallback(() => {
|
||||||
|
setWidthSize(window.innerWidth);
|
||||||
|
setHeightSize(window.innerHeight);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("resize", throttle(handleSize, 200));
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleSize);
|
||||||
|
};
|
||||||
|
}, [handleSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
widthSize={widthSize}
|
||||||
|
heightSize={heightSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSize;
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details> -->
|
||||||
|
|
||||||
|
- display: none으로 처리해도 DOM에는 사라지지 않기 때문에 상태 변경시 랜더링 시도함
|
||||||
|
|
||||||
|
- width 값을 측정하여 조건이 맞을 경우에만 컴포넌트를 랜더링 하게 함
|
||||||
|
- throttle 사용으로 과도한 width값 측정 방지
|
||||||
|
- ```javascript
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
|
||||||
|
const withSize = () => (OriginalComponent) => (props) => {
|
||||||
|
const [widthSize, setWidthSize] = useState(window.innerWidth);
|
||||||
|
const [heightSize, setHeightSize] = useState(window.innerHeight);
|
||||||
|
|
||||||
|
const handleSize = useCallback(() => {
|
||||||
|
setWidthSize(window.innerWidth);
|
||||||
|
setHeightSize(window.innerHeight);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("resize", throttle(handleSize, 200));
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleSize);
|
||||||
|
};
|
||||||
|
}, [handleSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
widthSize={widthSize}
|
||||||
|
heightSize={heightSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default withSize;
|
||||||
|
```
|
||||||
|
|
||||||
|
- 초기 차트 데이터를 얼마나 가져와야 하는지에 대한 문제
|
||||||
|
- 200개의 캔들을 먼저 가져오고 필요할 시 추가로 요청 후 랜더링
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
- [x] WebSocket 통신<br>
|
||||||
|
- [x] 기본 Reducer 제작<br>
|
||||||
|
- [x] Thunk Factory Pattern 제작<br>
|
||||||
|
- [x] Saga Factory Pattern 제작<br>
|
||||||
|
- [x] 캔들 차트 드로잉<br>
|
||||||
|
- [x] 호가 차트 드로잉<br>
|
||||||
|
- [x] 주문 창 구현<br>
|
||||||
71
package.json
Normal file
71
package.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "upbit-clone",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/polyfill": "^7.12.1",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.12",
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.3.2",
|
||||||
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
"axios": "^0.20.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"d3": "^5.15.1",
|
||||||
|
"d3-selection": "^1.1.0",
|
||||||
|
"d3-transition": "^1.3.2",
|
||||||
|
"d3fc": "^15.0.16",
|
||||||
|
"decimal.js": "^10.2.1",
|
||||||
|
"hangul-js": "^0.2.6",
|
||||||
|
"install": "^0.13.0",
|
||||||
|
"lodash": "^4.17.20",
|
||||||
|
"moment-timezone": "^0.5.31",
|
||||||
|
"npm": "^6.14.9",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-app-polyfill": "^2.0.0",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"react-fast-compare": "^3.2.0",
|
||||||
|
"react-financial-charts": "1.0.0-alpha.16",
|
||||||
|
"react-loading": "^2.0.3",
|
||||||
|
"react-redux": "^7.2.1",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-scripts": "3.2.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-devtools-extension": "^2.13.8",
|
||||||
|
"redux-saga": "^1.1.3",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"styled-components": "^5.2.0",
|
||||||
|
"styled-normalize": "^8.0.7",
|
||||||
|
"styled-reset": "^4.3.0",
|
||||||
|
"text-encoding": "^0.7.0",
|
||||||
|
"websocket": "^1.0.32"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
">0.2%",
|
||||||
|
"ie 9",
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/blueLogo.png
Normal file
BIN
public/blueLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 571 B |
20
public/index.html
Normal file
20
public/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="upbit clone project created by Seongkyun Yu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<title>Downbit</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
BIN
public/whiteLogo.png
Normal file
BIN
public/whiteLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
60
src/Api/api.js
Normal file
60
src/Api/api.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const coinApi = {
|
||||||
|
getMarketCodes: () =>
|
||||||
|
axios.get("https://api.upbit.com/v1/market/all?isDetails=false"),
|
||||||
|
getInitCanldes: (coins) =>
|
||||||
|
axios.get(`https://api.upbit.com/v1/ticker?markets=${coins}`),
|
||||||
|
getInitOrderbooks: (coins) =>
|
||||||
|
axios.get(`https://api.upbit.com/v1/orderbook?markets=${coins}`),
|
||||||
|
getOneCoinCandles: ({ coin, timeType, timeCount }) => {
|
||||||
|
if (timeType === "minutes")
|
||||||
|
return axios
|
||||||
|
.get(
|
||||||
|
`https://api.upbit.com/v1/candles/${timeType}/${timeCount}?market=${coin}&count=200`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
data: res.data.sort((a, b) => a.timestamp - b.timestamp),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
else
|
||||||
|
return axios
|
||||||
|
.get(
|
||||||
|
`https://api.upbit.com/v1/candles/${timeType}?market=${coin}&count=200`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
data: res.data.sort((a, b) => a.timestamp - b.timestamp),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getAdditionalCoinCandles: ({ coin, timeType, timeCount, datetime }) => {
|
||||||
|
if (timeType === "minutes")
|
||||||
|
return axios
|
||||||
|
.get(
|
||||||
|
`https://api.upbit.com/v1/candles/${timeType}/${timeCount}?market=${coin}&to=${datetime}&count=200`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
data: res.data.sort((a, b) => a.timestamp - b.timestamp),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
else
|
||||||
|
return axios
|
||||||
|
.get(
|
||||||
|
`https://api.upbit.com/v1/candles/${timeType}?market=${coin}&to=${datetime}&count=200`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
data: res.data.sort((a, b) => a.timestamp - b.timestamp),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getOneCoinTradeLists: (coin) =>
|
||||||
|
axios.get(`https://api.upbit.com/v1/trades/ticks?market=${coin}&count=50`),
|
||||||
|
};
|
||||||
15
src/App.js
Normal file
15
src/App.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { startInit } from "./Reducer/coinReducer";
|
||||||
|
import MainRouter from "./Router/MainRouter";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(startInit());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return <MainRouter />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
132
src/Components/Global/Footer.js
Normal file
132
src/Components/Global/Footer.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { faEnvelope } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Footer: styled.footer`
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/* height: 120px; */
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px 0;
|
||||||
|
@media ${({ theme }) => theme.tablet} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Container: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 1360px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.tablet} {
|
||||||
|
display: block;
|
||||||
|
max-width: 950px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
MainLink: styled.a`
|
||||||
|
display: block;
|
||||||
|
background-image: ${({ logo }) => `url(${logo})`};
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
color: transparent;
|
||||||
|
width: 130px;
|
||||||
|
height: 60px;
|
||||||
|
`,
|
||||||
|
Description: styled.p`
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: gray;
|
||||||
|
height: 85px;
|
||||||
|
margin-top: 10px;
|
||||||
|
/* margin-left: 250px; */
|
||||||
|
`,
|
||||||
|
DescSpan: styled.span`
|
||||||
|
display: block;
|
||||||
|
height: 30px;
|
||||||
|
`,
|
||||||
|
ContactContainer: styled.address`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* margin-left: 250px; */
|
||||||
|
`,
|
||||||
|
LinkTitle: styled.span`
|
||||||
|
height: 25px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: gray;
|
||||||
|
`,
|
||||||
|
LinkTag: styled.a`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
`,
|
||||||
|
LinkSpan: styled.span`
|
||||||
|
display: block;
|
||||||
|
margin-left: ${({ marginLeft }) => marginLeft || "8px"};
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
height: 20px;
|
||||||
|
/* line-height: 1.5rem; */
|
||||||
|
color: gray;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<St.Footer>
|
||||||
|
<St.Container>
|
||||||
|
<St.MainLink
|
||||||
|
href="/"
|
||||||
|
title={"메인으로 이동"}
|
||||||
|
logo={process.env.PUBLIC_URL + "/blueLogo.png"}
|
||||||
|
/>
|
||||||
|
<St.Description>
|
||||||
|
<St.DescSpan>Upbit Clone Project - Downbit</St.DescSpan>
|
||||||
|
<St.DescSpan>Created by Seongkyun Yu</St.DescSpan>
|
||||||
|
<St.DescSpan>
|
||||||
|
Copyright © 2020 DOWNBIT INC. ALL RIGHTS RESERVED.
|
||||||
|
</St.DescSpan>
|
||||||
|
</St.Description>
|
||||||
|
<St.ContactContainer>
|
||||||
|
<St.LinkTitle>Contact Me</St.LinkTitle>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<St.LinkTag href="https://github.com/Seongkyun-Yu/upbit-clone">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faGithub}
|
||||||
|
size="lg"
|
||||||
|
title={"Github 아이콘"}
|
||||||
|
/>
|
||||||
|
<St.LinkSpan>github.com/Seongkyun-Yu/upbit-clone</St.LinkSpan>
|
||||||
|
</St.LinkTag>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<St.LinkTag href="mailto:ysungkyun@gmail.com">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faEnvelope}
|
||||||
|
size="lg"
|
||||||
|
title={"이메일 아이콘"}
|
||||||
|
/>
|
||||||
|
<St.LinkSpan>ysungkyun@gmail.com</St.LinkSpan>
|
||||||
|
</St.LinkTag>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</St.ContactContainer>
|
||||||
|
</St.Container>
|
||||||
|
</St.Footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
64
src/Components/Global/Header.js
Normal file
64
src/Components/Global/Header.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Header: styled.header`
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background-color: rgb(9, 54, 135);
|
||||||
|
`,
|
||||||
|
Container: styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 1360px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media ${({ theme, isRootURL }) => (!isRootURL ? theme.tablet : true)} {
|
||||||
|
max-width: 950px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme, isRootURL }) => (isRootURL ? theme.tablet : true)} {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
SiteHeading: styled.h1`
|
||||||
|
padding: 0 20px;
|
||||||
|
width: 150px;
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
MainLink: styled.a`
|
||||||
|
display: block;
|
||||||
|
background-image: ${({ logo }) => `url(${logo})`};
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = ({ isRootURL }) => {
|
||||||
|
return (
|
||||||
|
<St.Header>
|
||||||
|
<St.Container isRootURL={isRootURL}>
|
||||||
|
<St.SiteHeading>
|
||||||
|
<St.MainLink
|
||||||
|
href="/"
|
||||||
|
logo={process.env.PUBLIC_URL + "/whiteLogo.png"}
|
||||||
|
title={"메인으로 이동"}
|
||||||
|
>
|
||||||
|
업비트
|
||||||
|
</St.MainLink>
|
||||||
|
</St.SiteHeading>
|
||||||
|
</St.Container>
|
||||||
|
</St.Header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
34
src/Components/Global/Loading.js
Normal file
34
src/Components/Global/Loading.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactLoading from "react-loading";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Container: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
${({ isCenter }) =>
|
||||||
|
!isCenter &&
|
||||||
|
css`
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: 200px;
|
||||||
|
`}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = ({ center = true }) => {
|
||||||
|
return (
|
||||||
|
<St.Container isCenter={center}>
|
||||||
|
<ReactLoading
|
||||||
|
type={"spokes"}
|
||||||
|
color={"rgb(18, 97, 196)"}
|
||||||
|
height={"100px"}
|
||||||
|
width={"100px"}
|
||||||
|
/>
|
||||||
|
</St.Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
119
src/Components/Main/ChartDataConsole.js
Normal file
119
src/Components/Main/ChartDataConsole.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import withSelectedOption from "../../Container/withSelectedOption";
|
||||||
|
import withThemeData from "../../Container/withThemeData";
|
||||||
|
import { changeTimeTypeAndData } from "../../Reducer/coinReducer";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Container: styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.lightGray2};
|
||||||
|
`,
|
||||||
|
HiddenH3: styled.h3`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
TimeBtnContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
`,
|
||||||
|
TimeBtn: styled.button`
|
||||||
|
/* width: 50px; */
|
||||||
|
height: 20px;
|
||||||
|
width: 38px;
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
border: ${({ theme, isSelected }) =>
|
||||||
|
isSelected ? `2px solid black` : `1px solid ${theme.lightGray2}`};
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartDataConsole = ({ theme, selectedTimeCount, selectedTimeType }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const changeChartTime = useCallback(
|
||||||
|
(timeCount, timeType) => () => {
|
||||||
|
dispatch(changeTimeTypeAndData({ timeCount, timeType }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<St.Container theme={theme}>
|
||||||
|
<St.HiddenH3>차트에 표시할 캔들의 시간 선택</St.HiddenH3>
|
||||||
|
<St.TimeBtnContainer>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(1, "minutes")}
|
||||||
|
isSelected={selectedTimeCount === 1 && selectedTimeType === "minutes"}
|
||||||
|
>
|
||||||
|
1m
|
||||||
|
</St.TimeBtn>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(3, "minutes")}
|
||||||
|
isSelected={selectedTimeCount === 3 && selectedTimeType === "minutes"}
|
||||||
|
>
|
||||||
|
3m
|
||||||
|
</St.TimeBtn>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(5, "minutes")}
|
||||||
|
isSelected={selectedTimeCount === 5 && selectedTimeType === "minutes"}
|
||||||
|
>
|
||||||
|
5m
|
||||||
|
</St.TimeBtn>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(10, "minutes")}
|
||||||
|
isSelected={
|
||||||
|
selectedTimeCount === 10 && selectedTimeType === "minutes"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
10m
|
||||||
|
</St.TimeBtn>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(15, "minutes")}
|
||||||
|
isSelected={
|
||||||
|
selectedTimeCount === 15 && selectedTimeType === "minutes"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
15m
|
||||||
|
</St.TimeBtn>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(60, "minutes")}
|
||||||
|
isSelected={
|
||||||
|
selectedTimeCount === 60 && selectedTimeType === "minutes"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
1h
|
||||||
|
</St.TimeBtn>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(240, "minutes")}
|
||||||
|
isSelected={
|
||||||
|
selectedTimeCount === 240 && selectedTimeType === "minutes"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
4h
|
||||||
|
</St.TimeBtn>
|
||||||
|
<St.TimeBtn
|
||||||
|
onClick={changeChartTime(1, "days")}
|
||||||
|
isSelected={selectedTimeCount === 1 && selectedTimeType === "days"}
|
||||||
|
>
|
||||||
|
1d
|
||||||
|
</St.TimeBtn>
|
||||||
|
</St.TimeBtnContainer>
|
||||||
|
</St.Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSelectedOption()(withThemeData()(ChartDataConsole));
|
||||||
213
src/Components/Main/CoinInfoHeader.js
Normal file
213
src/Components/Main/CoinInfoHeader.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import withSelectedCoinName from "../../Container/withSelectedCoinName";
|
||||||
|
import withSelectedCoinPrice from "../../Container/withSelectedCoinPrice";
|
||||||
|
import withThemeData from "../../Container/withThemeData";
|
||||||
|
import isEqual from "react-fast-compare";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
CoinInfoContainer: styled.section`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.lightGray2};
|
||||||
|
`,
|
||||||
|
HiddenH3: styled.h3`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
CoinInfoMain: styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 380px;
|
||||||
|
`,
|
||||||
|
CoinLogo: styled.i`
|
||||||
|
display: inline-block;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
background-image: ${({ coinSymbol }) =>
|
||||||
|
`url(https://static.upbit.com/logos/${coinSymbol}.png)`};
|
||||||
|
background-size: cover;
|
||||||
|
margin-left: 5px;
|
||||||
|
`,
|
||||||
|
CoinNameContainer: styled.div`
|
||||||
|
padding: 0 8px 0 13px;
|
||||||
|
`,
|
||||||
|
CoinName: styled.strong`
|
||||||
|
font-size: 1.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2b2b;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
CoinMarketName: styled.span`
|
||||||
|
display: flex;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 5px;
|
||||||
|
margin-top: 7px;
|
||||||
|
`,
|
||||||
|
PriceInfo: styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`,
|
||||||
|
Price: styled.strong`
|
||||||
|
color: ${({ priceColor }) => priceColor};
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
PriceUnit: styled.span`
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-left: 5px;
|
||||||
|
`,
|
||||||
|
ChangeContainer: styled.span`
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 5px;
|
||||||
|
`,
|
||||||
|
ChangeRate: styled.strong`
|
||||||
|
font-size: 1rem;
|
||||||
|
color: ${({ priceColor }) => priceColor};
|
||||||
|
margin: 0 10px 0 5px;
|
||||||
|
font-weight: 800;
|
||||||
|
`,
|
||||||
|
ChangePrice: styled.strong`
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: ${({ priceColor }) => priceColor};
|
||||||
|
`,
|
||||||
|
TradeInfoContainer: styled.dl`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
width: 45%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
|
||||||
|
@media ${({ theme, mobileMNone }) => (mobileMNone ? theme.mobileM : true)} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
InfoContainer: styled.div`
|
||||||
|
height: 100%;
|
||||||
|
margin-left: 15px;
|
||||||
|
@media ${({ theme, tabletNone }) => (tabletNone ? theme.tablet : true)} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media ${({ theme, mobileMNone }) => (mobileMNone ? theme.mobileM : true)} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
TradeInfo: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 50%;
|
||||||
|
min-width: ${({ minWidth }) => minWidth || "none"};
|
||||||
|
border-bottom: 1px solid ${({ borderColor }) => borderColor || "none"};
|
||||||
|
padding: 5px 0 5px 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
`,
|
||||||
|
TradeDT: styled.dt`
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 50px;
|
||||||
|
height: 50%;
|
||||||
|
`,
|
||||||
|
TradeDD: styled.dd`
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
height: 50%;
|
||||||
|
color: ${({ fontColor }) => fontColor || "black"};
|
||||||
|
font-weight: ${({ fontWeight }) => fontWeight || 500};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CoinInfoHeader = ({
|
||||||
|
theme,
|
||||||
|
coinNameKor,
|
||||||
|
coinSymbol,
|
||||||
|
coinNameAndMarketEng,
|
||||||
|
highestPrice24Hour,
|
||||||
|
lowestPrice24Hour,
|
||||||
|
changeRate24Hour,
|
||||||
|
changePrice24Hour,
|
||||||
|
tradePrice24Hour,
|
||||||
|
volume24Hour,
|
||||||
|
price,
|
||||||
|
}) => {
|
||||||
|
const priceColor = changeRate24Hour > 0 ? theme.priceUp : theme.priceDown;
|
||||||
|
return (
|
||||||
|
<St.CoinInfoContainer>
|
||||||
|
<St.HiddenH3>코인 가격 및 기타 정보</St.HiddenH3>
|
||||||
|
<St.CoinInfoMain>
|
||||||
|
<St.CoinLogo coinSymbol={coinSymbol} title={`${coinNameKor} 로고`} />
|
||||||
|
<St.CoinNameContainer>
|
||||||
|
<St.CoinName>{coinNameKor}</St.CoinName>
|
||||||
|
<St.CoinMarketName>{coinNameAndMarketEng}</St.CoinMarketName>
|
||||||
|
</St.CoinNameContainer>
|
||||||
|
<St.PriceInfo>
|
||||||
|
<St.Price priceColor={priceColor}>
|
||||||
|
{price.toLocaleString()}
|
||||||
|
<St.PriceUnit priceColor={priceColor}>KRW</St.PriceUnit>
|
||||||
|
</St.Price>
|
||||||
|
<St.ChangeContainer>
|
||||||
|
전일대비
|
||||||
|
<St.ChangeRate priceColor={priceColor}>
|
||||||
|
{changeRate24Hour}%
|
||||||
|
</St.ChangeRate>
|
||||||
|
<St.ChangePrice priceColor={priceColor}>
|
||||||
|
{changePrice24Hour.toLocaleString()}
|
||||||
|
</St.ChangePrice>
|
||||||
|
</St.ChangeContainer>
|
||||||
|
</St.PriceInfo>
|
||||||
|
</St.CoinInfoMain>
|
||||||
|
<St.TradeInfoContainer mobileMNone={true}>
|
||||||
|
<St.InfoContainer tabletNone={true}>
|
||||||
|
<St.TradeInfo minWidth={"100px"} borderColor={theme.lightGray2}>
|
||||||
|
<St.TradeDT>고가</St.TradeDT>
|
||||||
|
<St.TradeDD fontColor={theme.priceUp} fontWeight={800}>
|
||||||
|
{highestPrice24Hour ? highestPrice24Hour.toLocaleString() : 0}
|
||||||
|
</St.TradeDD>
|
||||||
|
</St.TradeInfo>
|
||||||
|
<St.TradeInfo minWidth={"100px"}>
|
||||||
|
<St.TradeDT borderColor={theme.lightGray2}>저가</St.TradeDT>
|
||||||
|
<St.TradeDD fontColor={theme.priceDown} fontWeight={800}>
|
||||||
|
{lowestPrice24Hour ? lowestPrice24Hour.toLocaleString() : 0}
|
||||||
|
</St.TradeDD>
|
||||||
|
</St.TradeInfo>
|
||||||
|
</St.InfoContainer>
|
||||||
|
<St.InfoContainer mobileMNone={true}>
|
||||||
|
<St.TradeInfo minWidth={"220px"} borderColor={theme.lightGray2}>
|
||||||
|
<St.TradeDT>거래량(24h)</St.TradeDT>
|
||||||
|
<St.TradeDD>{`${volume24Hour.toLocaleString()} ${coinSymbol}`}</St.TradeDD>
|
||||||
|
</St.TradeInfo>
|
||||||
|
<St.TradeInfo minWidth={"220px"}>
|
||||||
|
<St.TradeDT borderColor={theme.lightGray2}>
|
||||||
|
거래대금(24h)
|
||||||
|
</St.TradeDT>
|
||||||
|
<St.TradeDD>
|
||||||
|
{tradePrice24Hour ? tradePrice24Hour.toLocaleString() : 0} KRW
|
||||||
|
</St.TradeDD>
|
||||||
|
</St.TradeInfo>
|
||||||
|
</St.InfoContainer>
|
||||||
|
</St.TradeInfoContainer>
|
||||||
|
</St.CoinInfoContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSelectedCoinName()(
|
||||||
|
withSelectedCoinPrice()(withThemeData()(React.memo(CoinInfoHeader)))
|
||||||
|
);
|
||||||
210
src/Components/Main/CoinList.js
Normal file
210
src/Components/Main/CoinList.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { searchCoin } from "../../Reducer/coinReducer";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
|
import CoinListItem from "./CoinListItem";
|
||||||
|
import Loading from "../Global/Loading";
|
||||||
|
|
||||||
|
import withThemeData from "../../Container/withThemeData";
|
||||||
|
import withSelectedOption from "../../Container/withSelectedOption";
|
||||||
|
import withMarketNames from "../../Container/withMarketNames";
|
||||||
|
import withLatestCoinData from "../../Container/withLatestCoinData";
|
||||||
|
import withLoadingData from "../../Container/withLoadingData";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
CoinListContainer: styled.article`
|
||||||
|
display: none;
|
||||||
|
position: -webkit-sticky; /* 사파리 */
|
||||||
|
position: sticky;
|
||||||
|
top: 70px;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.desktop} {
|
||||||
|
display: block;
|
||||||
|
max-width: 400px;
|
||||||
|
height: ${({ heightSize }) => `${heightSize}px`};
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme, isRootURL }) => (!isRootURL ? theme.mobileM : true)} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme, isRootURL }) => (isRootURL ? theme.tablet : true)} {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
height: ${({ heightSize }) =>
|
||||||
|
`${heightSize + 80}px`}; // 모바일 풀 화면을 위해 다시 80px 더해줌
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HiddenH3: styled.h3`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
CoinSearchContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.lightGray2};
|
||||||
|
`,
|
||||||
|
CoinSearchInput: styled.input`
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
padding: 5px;
|
||||||
|
padding-left: 12px;
|
||||||
|
&::placeholder {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: gray;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
CoinSearchBtn: styled.button`
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background: url("https://cdn.upbit.com/images/bg.e801517.png") -83px 2px no-repeat;
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-left: 20px;
|
||||||
|
border: none;
|
||||||
|
`,
|
||||||
|
CoinSortContainer: styled.ul`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: white;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.lightGray2};
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #666666;
|
||||||
|
`,
|
||||||
|
CoinSortList: styled.li`
|
||||||
|
width: ${({ width }) => width || "20%"};
|
||||||
|
text-align: ${({ textAlign }) => textAlign || "right"};
|
||||||
|
margin-right: ${({ marginRight }) => marginRight || 0};
|
||||||
|
font-size: 0.78rem;
|
||||||
|
`,
|
||||||
|
CoinUl: styled.ul`
|
||||||
|
height: ${({ heightSize }) => `${heightSize + 70}px`};
|
||||||
|
min-height: 800px;
|
||||||
|
background-color: white;
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-color: ${({ theme }) => theme.middleGray};
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-base-color: ${({ theme }) => theme.middleGray};
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: ${({ theme }) => theme.middleGray};
|
||||||
|
border-radius: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.desktop} {
|
||||||
|
display: block;
|
||||||
|
max-width: 400px;
|
||||||
|
height: ${({ heightSize }) => `${heightSize}px`};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CoinList = ({
|
||||||
|
theme,
|
||||||
|
marketNames,
|
||||||
|
sortedMarketNames,
|
||||||
|
latestCoinData,
|
||||||
|
selectedMarket,
|
||||||
|
searchCoinInput,
|
||||||
|
isMarketNamesLoading,
|
||||||
|
isInitCandleLoading,
|
||||||
|
heightSize,
|
||||||
|
isRootURL,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<St.CoinListContainer isRootURL={isRootURL} heightSize={heightSize - 80}>
|
||||||
|
<St.HiddenH3>코인 리스트</St.HiddenH3>
|
||||||
|
<St.CoinSearchContainer>
|
||||||
|
<St.CoinSearchInput
|
||||||
|
type="search"
|
||||||
|
onChange={(e) => dispatch(searchCoin(e.target.value))}
|
||||||
|
value={searchCoinInput}
|
||||||
|
placeholder={"코인명/심볼검색"}
|
||||||
|
/>
|
||||||
|
<St.CoinSearchBtn />
|
||||||
|
</St.CoinSearchContainer>
|
||||||
|
<St.CoinSortContainer>
|
||||||
|
<St.CoinSortList width={"50px"} />
|
||||||
|
<St.CoinSortList textAlign={"left"}>한글명</St.CoinSortList>
|
||||||
|
<St.CoinSortList>현재가</St.CoinSortList>
|
||||||
|
<St.CoinSortList>상승률</St.CoinSortList>
|
||||||
|
<St.CoinSortList width={"25%"} marginRight={"10px"}>
|
||||||
|
거래대금
|
||||||
|
</St.CoinSortList>
|
||||||
|
</St.CoinSortContainer>
|
||||||
|
<St.CoinUl heightSize={heightSize - 140}>
|
||||||
|
{isMarketNamesLoading || isInitCandleLoading ? (
|
||||||
|
<Loading center={false} />
|
||||||
|
) : (
|
||||||
|
sortedMarketNames.map((marketName) => {
|
||||||
|
const splitedName = marketName.split("-");
|
||||||
|
const enCoinName = splitedName[1] + "/" + splitedName[0];
|
||||||
|
const changePrice24Hour =
|
||||||
|
latestCoinData[marketName].changePrice24Hour;
|
||||||
|
const changeRate24Hour =
|
||||||
|
latestCoinData[marketName].changeRate24Hour;
|
||||||
|
const tradePrice24Hour =
|
||||||
|
latestCoinData[marketName].tradePrice24Hour;
|
||||||
|
const price = latestCoinData[marketName].price;
|
||||||
|
// const isTraded = latestCoinData[marketName].isTraded;
|
||||||
|
|
||||||
|
const fontColor =
|
||||||
|
+changePrice24Hour > 0
|
||||||
|
? theme.strongRed
|
||||||
|
: +changePrice24Hour < 0
|
||||||
|
? theme.strongBlue
|
||||||
|
: "black";
|
||||||
|
return (
|
||||||
|
<CoinListItem
|
||||||
|
theme={theme}
|
||||||
|
marketName={marketName}
|
||||||
|
selectedMarket={selectedMarket}
|
||||||
|
coinName={marketNames[marketName].korean}
|
||||||
|
enCoinName={enCoinName}
|
||||||
|
fontColor={fontColor}
|
||||||
|
price={price}
|
||||||
|
changeRate24Hour={changeRate24Hour + "%"}
|
||||||
|
changePrice24Hour={changePrice24Hour}
|
||||||
|
tradePrice24Hour={tradePrice24Hour}
|
||||||
|
// isTraded={isTraded}
|
||||||
|
key={`coinList-${marketName}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</St.CoinUl>
|
||||||
|
</St.CoinListContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withLatestCoinData()(
|
||||||
|
withMarketNames()(
|
||||||
|
withSelectedOption()(
|
||||||
|
withLoadingData()(withThemeData()(React.memo(CoinList)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
206
src/Components/Main/CoinListItem.js
Normal file
206
src/Components/Main/CoinListItem.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
import { startChangeMarketAndData } from "../../Reducer/coinReducer";
|
||||||
|
|
||||||
|
import isEqual from "react-fast-compare";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
CoinLi: styled.li`
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
|
||||||
|
border-bottom: 1px solid ${({ borderBottomColor }) => borderBottomColor};
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
background-color: ${({ bgColor }) => bgColor};
|
||||||
|
`,
|
||||||
|
CoinBtn: styled.button`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
`,
|
||||||
|
CoinLogo: styled.i`
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-image: ${({ coinNameEn }) =>
|
||||||
|
coinNameEn !== "ADX"
|
||||||
|
? `url(https://static.upbit.com/logos/${coinNameEn}.png)`
|
||||||
|
: "../styles/img/ADX.png"};
|
||||||
|
background-size: cover;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 15px;
|
||||||
|
`,
|
||||||
|
CoinNameContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20%;
|
||||||
|
min-width: 55px;
|
||||||
|
height: 45px;
|
||||||
|
`,
|
||||||
|
CoinName: styled.strong`
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
@media ${({ theme }) => theme.tablet} {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
CoinNameEn: styled.span`
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
`,
|
||||||
|
Price: styled.strong`
|
||||||
|
display: block;
|
||||||
|
width: 20%;
|
||||||
|
min-width: 55px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: ${({ fontColor }) => fontColor};
|
||||||
|
|
||||||
|
border: 1px solid transparent;
|
||||||
|
${({ isTraded }) =>
|
||||||
|
isTraded &&
|
||||||
|
(isTraded === "ASK"
|
||||||
|
? css`
|
||||||
|
animation: disappearBlue 0.6s;
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
animation: disappearRed 0.6s;
|
||||||
|
`)};
|
||||||
|
@keyframes disappearBlue {
|
||||||
|
0% {
|
||||||
|
border-color: ${({ theme }) => theme.strongBlue};
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: ${({ theme }) => theme.strongBlue};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes disappearRed {
|
||||||
|
0% {
|
||||||
|
border-color: ${({ theme }) => theme.strongRed};
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: ${({ theme }) => theme.strongRed};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.tablet} {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
ChangRateContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20%;
|
||||||
|
min-width: 55px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: right;
|
||||||
|
`,
|
||||||
|
ChangeRate: styled.span`
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${({ fontColor }) => fontColor};
|
||||||
|
/* font-weight: 800; */
|
||||||
|
`,
|
||||||
|
ChangePrice: styled.span`
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${({ fontColor }) => fontColor};
|
||||||
|
/* font-weight: 800; */
|
||||||
|
`,
|
||||||
|
TradePrice: styled.span`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 25%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: right;
|
||||||
|
/* font-weight: 800; */
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CoinListItem = ({
|
||||||
|
theme,
|
||||||
|
selectedMarket,
|
||||||
|
marketName,
|
||||||
|
coinName,
|
||||||
|
enCoinName,
|
||||||
|
fontColor,
|
||||||
|
price,
|
||||||
|
changeRate24Hour,
|
||||||
|
changePrice24Hour,
|
||||||
|
tradePrice24Hour,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const tradeListData = useSelector(
|
||||||
|
(state) => state.Coin.tradeList.data[marketName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const nowTimestamp = +new Date();
|
||||||
|
|
||||||
|
const isTraded =
|
||||||
|
tradeListData &&
|
||||||
|
tradeListData.length > 2 &&
|
||||||
|
nowTimestamp - tradeListData[0].timestamp < 500 &&
|
||||||
|
tradeListData[0].trade_price !== tradeListData[1].trade_price
|
||||||
|
? tradeListData[0].ask_bid
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const changeMarket = useCallback(() => {
|
||||||
|
dispatch(startChangeMarketAndData(marketName));
|
||||||
|
history.push("/trade");
|
||||||
|
}, [dispatch, marketName, history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<St.CoinLi
|
||||||
|
borderBottomColor={theme.lightGray}
|
||||||
|
bgColor={selectedMarket === marketName ? theme.lightGray : "white"}
|
||||||
|
>
|
||||||
|
<St.CoinBtn onClick={changeMarket}>
|
||||||
|
<St.CoinLogo
|
||||||
|
coinNameEn={enCoinName.split("/")[0]}
|
||||||
|
title={`${coinName} 로고`}
|
||||||
|
/>
|
||||||
|
<St.CoinNameContainer>
|
||||||
|
<St.CoinName theme={theme}>{coinName}</St.CoinName>
|
||||||
|
<St.CoinNameEn>{enCoinName}</St.CoinNameEn>
|
||||||
|
</St.CoinNameContainer>
|
||||||
|
<St.Price theme={theme} fontColor={fontColor} isTraded={isTraded}>
|
||||||
|
{price.toLocaleString()}
|
||||||
|
</St.Price>
|
||||||
|
<St.ChangRateContainer>
|
||||||
|
<St.ChangeRate fontColor={fontColor}>
|
||||||
|
{changeRate24Hour}
|
||||||
|
</St.ChangeRate>
|
||||||
|
<St.ChangePrice fontColor={fontColor}>
|
||||||
|
{changePrice24Hour.toLocaleString()}
|
||||||
|
</St.ChangePrice>
|
||||||
|
</St.ChangRateContainer>
|
||||||
|
<St.TradePrice>
|
||||||
|
{tradePrice24Hour.toLocaleString() + " 백만"}
|
||||||
|
</St.TradePrice>
|
||||||
|
</St.CoinBtn>
|
||||||
|
</St.CoinLi>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(CoinListItem, isEqual);
|
||||||
78
src/Components/Main/MainChart-d3fc.js
Normal file
78
src/Components/Main/MainChart-d3fc.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import * as fc from "d3fc";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const MainChart = () => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket);
|
||||||
|
// const selectedCandles = useSelector(
|
||||||
|
// (state) => state.Coin.candle.data[selectedMarket].candles
|
||||||
|
// );
|
||||||
|
// console.log(test[0].date);
|
||||||
|
|
||||||
|
const selectedCandles = fc.randomFinancial()(200);
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
if (selectedCandles.length > 2) {
|
||||||
|
let updated = true;
|
||||||
|
|
||||||
|
// console.log("여기야", selectedCandles);
|
||||||
|
const xExtent = fc.extentDate().accessors([(d) => d.date]);
|
||||||
|
const yExtent = fc.extentLinear().accessors([(d) => d.high, (d) => d.low]);
|
||||||
|
|
||||||
|
const gridlines = fc.annotationSvgGridline();
|
||||||
|
|
||||||
|
const candlestick = fc.seriesSvgCandlestick().decorate((sel) => {
|
||||||
|
sel.enter().style("fill", (_, i) => (_.open > _.close ? "blue" : "red"));
|
||||||
|
sel
|
||||||
|
.enter()
|
||||||
|
.style("stroke", (_, i) => (_.open > _.close ? "blue" : "red"));
|
||||||
|
});
|
||||||
|
|
||||||
|
const multi = fc.seriesSvgMulti().series([gridlines, candlestick]);
|
||||||
|
// 줌용 설정
|
||||||
|
const x = d3.scaleTime();
|
||||||
|
const y = d3.scaleLinear();
|
||||||
|
|
||||||
|
const x2 = d3.scaleTime();
|
||||||
|
const y2 = d3.scaleLinear();
|
||||||
|
|
||||||
|
const zoom = d3.zoom().on("zoom", () => {
|
||||||
|
// update the scale used by the chart to use the updated domain
|
||||||
|
|
||||||
|
x.domain(d3.event.transform.rescaleX(x2).domain());
|
||||||
|
y.domain(d3.event.transform.rescaleY(y2).domain());
|
||||||
|
d3.select(".chartContainer").datum(selectedCandles).call(chart);
|
||||||
|
});
|
||||||
|
const chart = fc
|
||||||
|
.chartCartesian(x, y)
|
||||||
|
.xDomain(xExtent(selectedCandles))
|
||||||
|
.yDomain(yExtent(selectedCandles))
|
||||||
|
.svgPlotArea(multi)
|
||||||
|
.decorate((sel) => {
|
||||||
|
sel
|
||||||
|
.enter()
|
||||||
|
.select(".plot-area")
|
||||||
|
.on("measure.range", () => {
|
||||||
|
x2.range([0, d3.event.detail.width]);
|
||||||
|
y2.range([d3.event.detail.height, 0]);
|
||||||
|
})
|
||||||
|
.call(zoom);
|
||||||
|
});
|
||||||
|
|
||||||
|
x2.domain(chart.xDomain());
|
||||||
|
y2.domain(chart.yDomain());
|
||||||
|
|
||||||
|
d3.select(".chartContainer").datum(selectedCandles).call(chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {}, [selectedCandles]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chartContainer">
|
||||||
|
{/* <svg id="chartContainer" ref={svgRef}></svg> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainChart;
|
||||||
180
src/Components/Main/MainChart-old.js
Normal file
180
src/Components/Main/MainChart-old.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import * as fc from "d3fc";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const MainChart = () => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket);
|
||||||
|
const selectedCandles = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket].candles
|
||||||
|
);
|
||||||
|
const dateFormat = d3.timeParse("%Y-%m-%d %H:%M");
|
||||||
|
|
||||||
|
const svgRef = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const margin = { top: 15, right: 65, bottom: 205, left: 50 };
|
||||||
|
const width = 1000 - margin.left - margin.right;
|
||||||
|
const height = 625 - margin.top - margin.bottom;
|
||||||
|
|
||||||
|
d3.select("#chartContainer").remove();
|
||||||
|
const svg = d3
|
||||||
|
.select("#root")
|
||||||
|
.append("svg")
|
||||||
|
.attr("id", "chartContainer")
|
||||||
|
.attr("width", width + margin.left + margin.right)
|
||||||
|
.attr("height", height + margin.top + margin.bottom)
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||||
|
|
||||||
|
const dates = selectedCandles.map((candle) => dateFormat(candle.datetime));
|
||||||
|
|
||||||
|
const xScale = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([-1, dates.length])
|
||||||
|
.range([0, width]);
|
||||||
|
|
||||||
|
const xDateScale = d3
|
||||||
|
.scaleQuantize()
|
||||||
|
.domain([0, dates.length])
|
||||||
|
.range(dates.length ? dates : ["20-01-01 00:00"]);
|
||||||
|
|
||||||
|
const xBand = d3
|
||||||
|
.scaleBand()
|
||||||
|
.domain(d3.range(-1, dates.length))
|
||||||
|
.range([0, width])
|
||||||
|
.padding(0.3);
|
||||||
|
|
||||||
|
const xAxis = d3.axisBottom().scale(xScale);
|
||||||
|
|
||||||
|
svg
|
||||||
|
.append("rect")
|
||||||
|
.attr("id", "rect")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height)
|
||||||
|
.style("fill", "none")
|
||||||
|
.style("pointer-events", "all")
|
||||||
|
.attr("clip-path", "url(#clip)");
|
||||||
|
|
||||||
|
let gX = svg
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "axis x-axis") //Assign "axis" class
|
||||||
|
.attr("transform", "translate(0," + height + ")")
|
||||||
|
.call(xAxis);
|
||||||
|
|
||||||
|
gX.selectAll(".tick text");
|
||||||
|
|
||||||
|
const ymin = d3.min(selectedCandles.map((candle) => candle.low));
|
||||||
|
const ymax = d3.max(selectedCandles.map((candle) => candle.high));
|
||||||
|
const yScale = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([ymin, ymax])
|
||||||
|
.range([height, 0])
|
||||||
|
.nice();
|
||||||
|
const yAxis = d3.axisLeft().scale(yScale);
|
||||||
|
|
||||||
|
const gY = svg.append("g").attr("class", "axis y-axis").call(yAxis);
|
||||||
|
|
||||||
|
const chartBody = svg
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "chartBody")
|
||||||
|
.attr("clip-path", "url(#clip)");
|
||||||
|
|
||||||
|
// 캔들 몸통
|
||||||
|
const candles = chartBody.selectAll(".candle").data(selectedCandles);
|
||||||
|
candles
|
||||||
|
.enter()
|
||||||
|
.append("rect")
|
||||||
|
.attr("x", (_, i) => xScale(i) - xBand.bandwidth())
|
||||||
|
.attr("class", "candle")
|
||||||
|
.attr("y", (candle) => yScale(Math.max(candle.open, candle.close)))
|
||||||
|
.attr("width", xBand.bandwidth())
|
||||||
|
.attr("height", (candle) => {
|
||||||
|
return candle.open === candle.close
|
||||||
|
? 1
|
||||||
|
: yScale(Math.min(candle.open, candle.close)) -
|
||||||
|
yScale(Math.max(candle.open, candle.close));
|
||||||
|
})
|
||||||
|
.attr("fill", (candle) => {
|
||||||
|
return candle.open === candle.close
|
||||||
|
? "silver"
|
||||||
|
: candle.open > candle.close
|
||||||
|
? "red"
|
||||||
|
: "green";
|
||||||
|
});
|
||||||
|
// candles.exit().remove();
|
||||||
|
|
||||||
|
// 윗꼬리 아랫꼬리
|
||||||
|
const stems = chartBody.selectAll("g.line").data(selectedCandles);
|
||||||
|
|
||||||
|
stems
|
||||||
|
.enter()
|
||||||
|
.append("line")
|
||||||
|
.attr("class", "stem")
|
||||||
|
.attr("x1", (_, i) => xScale(i) - xBand.bandwidth() / 2)
|
||||||
|
.attr("x2", (_, i) => xScale(i) - xBand.bandwidth() / 2)
|
||||||
|
.attr("y1", (candle) => yScale(candle.high))
|
||||||
|
.attr("y2", (candle) => yScale(candle.low))
|
||||||
|
.attr("stroke", (candle) => {
|
||||||
|
return candle.open === candle.close
|
||||||
|
? "white"
|
||||||
|
: candle.open > candle.close
|
||||||
|
? "red"
|
||||||
|
: "green";
|
||||||
|
});
|
||||||
|
// stems.exit().remove();
|
||||||
|
|
||||||
|
svg
|
||||||
|
.append("defs")
|
||||||
|
.append("clipPath")
|
||||||
|
.attr("id", "clip")
|
||||||
|
.append("rect")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height);
|
||||||
|
|
||||||
|
// d3.getEvent = () => require("d3-selection").event;
|
||||||
|
// const zoomed = () => {
|
||||||
|
// const t = currentEvent.transform;
|
||||||
|
// const xScaleZ = t.rescaleX(xScale);
|
||||||
|
|
||||||
|
// const hideTicksWithoutLabel = () => {
|
||||||
|
// d3.selectAll(".xAxis .tick text").each((d) => {
|
||||||
|
// if (this.innerHTML === "") {
|
||||||
|
// this.parentNode.style.display = "none";
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// gX.call(d3.axisBottom(xScaleZ));
|
||||||
|
|
||||||
|
// candles
|
||||||
|
// .attr("x", (d, i) => xScaleZ(i) - (xBand.bandwidth() * t.k) / 2)
|
||||||
|
// .attr("width", xBand.bandwidth() * t.k);
|
||||||
|
// stems.attr(
|
||||||
|
// "x1",
|
||||||
|
// (d, i) => xScaleZ(i) - xBand.bandwidth() / 2 + xBand.bandwidth() * 0.5
|
||||||
|
// );
|
||||||
|
// stems.attr(
|
||||||
|
// "x2",
|
||||||
|
// (d, i) => xScaleZ(i) - xBand.bandwidth() / 2 + xBand.bandwidth() * 0.5
|
||||||
|
// );
|
||||||
|
|
||||||
|
// hideTicksWithoutLabel();
|
||||||
|
|
||||||
|
// // gX.selectAll(".tick text").call(wrap, xBand.bandwidth());
|
||||||
|
// };
|
||||||
|
|
||||||
|
const extent = [
|
||||||
|
[0, 0],
|
||||||
|
[width, height],
|
||||||
|
];
|
||||||
|
|
||||||
|
let resizeTimer;
|
||||||
|
|
||||||
|
chartBody.exit().remove();
|
||||||
|
}, [selectedCandles]);
|
||||||
|
|
||||||
|
return <svg id="chartContainer" ref={svgRef}></svg>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainChart;
|
||||||
285
src/Components/Main/MainChart.js
Normal file
285
src/Components/Main/MainChart.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { startAddMoreCandleData } from "../../Reducer/coinReducer";
|
||||||
|
|
||||||
|
import { format } from "d3-format";
|
||||||
|
import { timeFormat } from "d3-time-format";
|
||||||
|
import {
|
||||||
|
elderRay,
|
||||||
|
ema,
|
||||||
|
discontinuousTimeScaleProviderBuilder,
|
||||||
|
Chart,
|
||||||
|
ChartCanvas,
|
||||||
|
CurrentCoordinate,
|
||||||
|
BarSeries,
|
||||||
|
CandlestickSeries,
|
||||||
|
ElderRaySeries,
|
||||||
|
LineSeries,
|
||||||
|
MovingAverageTooltip,
|
||||||
|
OHLCTooltip,
|
||||||
|
SingleValueTooltip,
|
||||||
|
mouseBasedZoomAnchor,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CrossHairCursor,
|
||||||
|
EdgeIndicator,
|
||||||
|
MouseCoordinateX,
|
||||||
|
MouseCoordinateY,
|
||||||
|
ZoomButtons,
|
||||||
|
withDeviceRatio,
|
||||||
|
withSize,
|
||||||
|
} from "react-financial-charts";
|
||||||
|
|
||||||
|
import Loading from "../Global/Loading";
|
||||||
|
|
||||||
|
import withOHLCData from "../../Container/withOHLCData";
|
||||||
|
import withThemeData from "../../Container/withThemeData";
|
||||||
|
import withSelectedOption from "../../Container/withSelectedOption";
|
||||||
|
import withLoadingData from "../../Container/withLoadingData";
|
||||||
|
import isEqual from "react-fast-compare";
|
||||||
|
|
||||||
|
const barChartExtents = (data) => {
|
||||||
|
return data.volume;
|
||||||
|
};
|
||||||
|
|
||||||
|
const candleChartExtents = (data) => {
|
||||||
|
return [data.high, data.low];
|
||||||
|
};
|
||||||
|
|
||||||
|
const yEdgeIndicator = (data) => {
|
||||||
|
return data.close;
|
||||||
|
};
|
||||||
|
|
||||||
|
const volumeSeries = (data) => {
|
||||||
|
return data.volume;
|
||||||
|
};
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
ChartContainer: styled.section`
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
background-color: white;
|
||||||
|
`,
|
||||||
|
HiddenH3: styled.h3`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const margin = { left: 10, right: 80, top: 20, bottom: 20 };
|
||||||
|
const minHeight = 350;
|
||||||
|
|
||||||
|
const MainChart = ({
|
||||||
|
data: initialData,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
ratio,
|
||||||
|
selectedTimeType,
|
||||||
|
theme,
|
||||||
|
isCandleLoading,
|
||||||
|
}) => {
|
||||||
|
if (height > 500) height = 500;
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dateTimeFormat =
|
||||||
|
selectedTimeType === "days" || selectedTimeType === "weeks"
|
||||||
|
? "%y-%m-%d"
|
||||||
|
: "%y-%m-%d %H:%M";
|
||||||
|
const timeDisplayFormat = timeFormat(dateTimeFormat);
|
||||||
|
const pricesDisplayFormat = format("");
|
||||||
|
|
||||||
|
const openCloseColor = (data) => {
|
||||||
|
return data.close > data.open ? theme.priceUp : theme.priceDown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const volumeColor = (data) => {
|
||||||
|
return data.close > data.open ? theme.priceUpTrans : theme.priceDownTrans;
|
||||||
|
};
|
||||||
|
|
||||||
|
const xScaleProvider = discontinuousTimeScaleProviderBuilder().inputDateAccessor(
|
||||||
|
(d) => d.date
|
||||||
|
);
|
||||||
|
|
||||||
|
const ema12 = ema()
|
||||||
|
.id(1)
|
||||||
|
.options({ windowSize: 12 })
|
||||||
|
.merge((d, c) => {
|
||||||
|
d.ema12 = c;
|
||||||
|
})
|
||||||
|
.accessor((d) => d.ema12);
|
||||||
|
|
||||||
|
const ema26 = ema()
|
||||||
|
.id(2)
|
||||||
|
.options({ windowSize: 26 })
|
||||||
|
.merge((d, c) => {
|
||||||
|
d.ema26 = c;
|
||||||
|
})
|
||||||
|
.accessor((d) => d.ema26);
|
||||||
|
|
||||||
|
const elder = elderRay();
|
||||||
|
|
||||||
|
const calculatedData = elder(ema26(ema12(initialData)));
|
||||||
|
|
||||||
|
const { data, xScale, xAccessor, displayXAccessor } = xScaleProvider(
|
||||||
|
calculatedData
|
||||||
|
);
|
||||||
|
|
||||||
|
// 확대 축소 초기 범위를 정하는 xExtendts설정, max와 min이 변하면 새로운 데이터 추가시 줌 설정이 풀린다
|
||||||
|
const max = xAccessor(data[Math.min(199, data.length - 1)]);
|
||||||
|
const min = xAccessor(
|
||||||
|
data.length < 50 ? 0 : data[Math.min(50, Math.floor(data.length / 2))]
|
||||||
|
);
|
||||||
|
const xExtents = [min, max + 5];
|
||||||
|
|
||||||
|
const gridHeight = height - margin.top - margin.bottom;
|
||||||
|
|
||||||
|
const elderRayHeight = 100;
|
||||||
|
const elderRayOrigin = (_, h) => [0, h - elderRayHeight];
|
||||||
|
const barChartHeight = gridHeight / 4;
|
||||||
|
const barChartOrigin = (_, h) => [0, h - barChartHeight - elderRayHeight];
|
||||||
|
const chartHeight = gridHeight - elderRayHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<St.ChartContainer>
|
||||||
|
<St.HiddenH3>캔들 차트</St.HiddenH3>
|
||||||
|
{isCandleLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
<ChartCanvas
|
||||||
|
height={height}
|
||||||
|
ratio={ratio}
|
||||||
|
width={width}
|
||||||
|
margin={margin}
|
||||||
|
data={data}
|
||||||
|
displayXAccessor={displayXAccessor}
|
||||||
|
seriesName="Data"
|
||||||
|
xScale={xScale}
|
||||||
|
xAccessor={xAccessor}
|
||||||
|
xExtents={xExtents}
|
||||||
|
disableInteraction={false}
|
||||||
|
zoomAnchor={mouseBasedZoomAnchor}
|
||||||
|
onLoadBefore={() => {
|
||||||
|
dispatch(startAddMoreCandleData());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chart
|
||||||
|
id={2}
|
||||||
|
height={barChartHeight}
|
||||||
|
origin={barChartOrigin}
|
||||||
|
yExtents={barChartExtents}
|
||||||
|
>
|
||||||
|
<BarSeries fillStyle={volumeColor} yAccessor={volumeSeries} />
|
||||||
|
</Chart>
|
||||||
|
<Chart id={3} height={chartHeight} yExtents={candleChartExtents}>
|
||||||
|
<XAxis showGridLines showTickLabel={false} />
|
||||||
|
<YAxis showGridLines tickFormat={pricesDisplayFormat} />
|
||||||
|
<CandlestickSeries
|
||||||
|
fill={openCloseColor}
|
||||||
|
wickStroke={openCloseColor}
|
||||||
|
/>
|
||||||
|
<LineSeries
|
||||||
|
yAccessor={ema26.accessor()}
|
||||||
|
strokeStyle={ema26.stroke()}
|
||||||
|
/>
|
||||||
|
<CurrentCoordinate
|
||||||
|
yAccessor={ema26.accessor()}
|
||||||
|
fillStyle={ema26.stroke()}
|
||||||
|
/>
|
||||||
|
<LineSeries
|
||||||
|
yAccessor={ema12.accessor()}
|
||||||
|
strokeStyle={ema12.stroke()}
|
||||||
|
/>
|
||||||
|
<CurrentCoordinate
|
||||||
|
yAccessor={ema12.accessor()}
|
||||||
|
fillStyle={ema12.stroke()}
|
||||||
|
/>
|
||||||
|
<MouseCoordinateY
|
||||||
|
rectWidth={margin.right}
|
||||||
|
displayFormat={pricesDisplayFormat}
|
||||||
|
/>
|
||||||
|
<EdgeIndicator
|
||||||
|
itemType="last"
|
||||||
|
rectWidth={margin.right}
|
||||||
|
fill={openCloseColor}
|
||||||
|
lineStroke={openCloseColor}
|
||||||
|
displayFormat={pricesDisplayFormat}
|
||||||
|
yAccessor={yEdgeIndicator}
|
||||||
|
/>
|
||||||
|
<MovingAverageTooltip
|
||||||
|
origin={[8, 24]}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
yAccessor: ema26.accessor(),
|
||||||
|
type: "EMA",
|
||||||
|
stroke: ema26.stroke(),
|
||||||
|
windowSize: ema26.options().windowSize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
yAccessor: ema12.accessor(),
|
||||||
|
type: "EMA",
|
||||||
|
stroke: ema12.stroke(),
|
||||||
|
windowSize: ema12.options().windowSize,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ZoomButtons />
|
||||||
|
<OHLCTooltip origin={[8, 16]} />
|
||||||
|
</Chart>
|
||||||
|
<Chart
|
||||||
|
id={4}
|
||||||
|
height={elderRayHeight}
|
||||||
|
yExtents={[0, elder.accessor()]}
|
||||||
|
origin={elderRayOrigin}
|
||||||
|
padding={{ top: 8, bottom: 8 }}
|
||||||
|
>
|
||||||
|
<XAxis showGridLines gridLinesStrokeStyle="#e0e3eb" />
|
||||||
|
<YAxis ticks={4} tickFormat={pricesDisplayFormat} />
|
||||||
|
|
||||||
|
<MouseCoordinateX displayFormat={timeDisplayFormat} />
|
||||||
|
<MouseCoordinateY
|
||||||
|
rectWidth={margin.right}
|
||||||
|
displayFormat={pricesDisplayFormat}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElderRaySeries yAccessor={elder.accessor()} />
|
||||||
|
|
||||||
|
<SingleValueTooltip
|
||||||
|
yAccessor={elder.accessor()}
|
||||||
|
yLabel="Elder Ray"
|
||||||
|
yDisplayFormat={(d) =>
|
||||||
|
`${pricesDisplayFormat(d.bullPower)}, ${pricesDisplayFormat(
|
||||||
|
d.bearPower
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
origin={[8, 16]}
|
||||||
|
/>
|
||||||
|
</Chart>
|
||||||
|
<CrossHairCursor snapX={false} />
|
||||||
|
</ChartCanvas>
|
||||||
|
)}
|
||||||
|
</St.ChartContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withOHLCData()(
|
||||||
|
withSize({
|
||||||
|
style: {
|
||||||
|
width: "100%",
|
||||||
|
height: "500",
|
||||||
|
minHeight,
|
||||||
|
},
|
||||||
|
})(
|
||||||
|
withDeviceRatio()(
|
||||||
|
withSelectedOption()(
|
||||||
|
withLoadingData()(withThemeData()(React.memo(MainChart, isEqual)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
110
src/Components/Main/OrderInfo.js
Normal file
110
src/Components/Main/OrderInfo.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import OrderInfoAskBid from "./OrderInfoAskBid";
|
||||||
|
|
||||||
|
import withSelectedOption from "../../Container/withSelectedOption";
|
||||||
|
import withThemeData from "../../Container/withThemeData";
|
||||||
|
import withSelectedCoinName from "../../Container/withSelectedCoinName";
|
||||||
|
|
||||||
|
import { changeAskBidOrder } from "../../Reducer/coinReducer";
|
||||||
|
import isEqual from "react-fast-compare";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Container: styled.section`
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
background-color: white;
|
||||||
|
`,
|
||||||
|
HiddenH3: styled.h3`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
OrderTypeContainer: styled.ul`
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.lightGray2};
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
OrderTypeLi: styled.li`
|
||||||
|
width: 33.3333%;
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
OrderTypeBtn: styled.button`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: white;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid ${({ borderBottom }) => borderBottom || "white"};
|
||||||
|
outline: 0;
|
||||||
|
font-weight: 900;
|
||||||
|
color: ${({ fontColor }) => fontColor || "black"};
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderInfo = ({
|
||||||
|
theme,
|
||||||
|
selectedAskBidOrder,
|
||||||
|
coinSymbol,
|
||||||
|
orderPrice,
|
||||||
|
orderAmount,
|
||||||
|
orderTotalPrice,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return (
|
||||||
|
<St.Container>
|
||||||
|
<St.HiddenH3>주문 정보</St.HiddenH3>
|
||||||
|
<St.OrderTypeContainer>
|
||||||
|
<St.OrderTypeLi>
|
||||||
|
<St.OrderTypeBtn
|
||||||
|
borderBottom={selectedAskBidOrder === "bid" && theme.strongRed}
|
||||||
|
fontColor={selectedAskBidOrder === "bid" && theme.strongRed}
|
||||||
|
onClick={() => dispatch(changeAskBidOrder("bid"))}
|
||||||
|
>
|
||||||
|
매수
|
||||||
|
</St.OrderTypeBtn>
|
||||||
|
</St.OrderTypeLi>
|
||||||
|
<St.OrderTypeLi>
|
||||||
|
<St.OrderTypeBtn
|
||||||
|
borderBottom={selectedAskBidOrder === "ask" && theme.strongBlue}
|
||||||
|
fontColor={selectedAskBidOrder === "ask" && theme.strongBlue}
|
||||||
|
onClick={() => dispatch(changeAskBidOrder("ask"))}
|
||||||
|
>
|
||||||
|
매도
|
||||||
|
</St.OrderTypeBtn>
|
||||||
|
</St.OrderTypeLi>
|
||||||
|
<St.OrderTypeLi>
|
||||||
|
<St.OrderTypeBtn
|
||||||
|
borderBottom={selectedAskBidOrder === "tradeList" && "black"}
|
||||||
|
fontColor={selectedAskBidOrder === "tradeList" && "black"}
|
||||||
|
onClick={() => dispatch(changeAskBidOrder("tradeList"))}
|
||||||
|
>
|
||||||
|
거래내역
|
||||||
|
</St.OrderTypeBtn>
|
||||||
|
</St.OrderTypeLi>
|
||||||
|
</St.OrderTypeContainer>
|
||||||
|
<OrderInfoAskBid
|
||||||
|
theme={theme}
|
||||||
|
selectedAskBidOrder={selectedAskBidOrder}
|
||||||
|
coinSymbol={coinSymbol}
|
||||||
|
orderPrice={orderPrice}
|
||||||
|
orderAmount={orderAmount}
|
||||||
|
orderTotalPrice={orderTotalPrice}
|
||||||
|
/>
|
||||||
|
</St.Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSelectedCoinName()(
|
||||||
|
withSelectedOption()(withThemeData()(React.memo(OrderInfo, isEqual)))
|
||||||
|
);
|
||||||
255
src/Components/Main/OrderInfoAskBid.js
Normal file
255
src/Components/Main/OrderInfoAskBid.js
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import {
|
||||||
|
changeAmountAndTotalPrice,
|
||||||
|
changePriceAndTotalPrice,
|
||||||
|
changeTotalPriceAndAmount,
|
||||||
|
} from "../../Reducer/coinReducer";
|
||||||
|
import OrderInfoTradeList from "./OrderInfoTradeList";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Container: styled.section`
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
background-color: white;
|
||||||
|
`,
|
||||||
|
OrderTypeContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.lightGray2};
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
OrderType: styled.button`
|
||||||
|
width: 33.33%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: white;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid
|
||||||
|
${({ borderBottom }) => borderBottom || "tranceparent"};
|
||||||
|
outline: 0;
|
||||||
|
font-weight: 900;
|
||||||
|
color: ${({ fontColor }) => fontColor || "black"};
|
||||||
|
`,
|
||||||
|
OrderInfoContainer: styled.div`
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
OrderInfoDetailContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
margint-right: 10px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
OrderInfoDetailTitle: styled.span`
|
||||||
|
display: block;
|
||||||
|
width: 20%;
|
||||||
|
min-width: 52px;
|
||||||
|
max-width: 100px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
`,
|
||||||
|
OrderInfoInputContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
OrderInfoInput: styled.input`
|
||||||
|
width: ${({ width }) => width || "100%"};
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
padding-right: 15px;
|
||||||
|
border: 1px solid ${({ theme }) => theme.lightGray2};
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: ${({ fontWeight }) => fontWeight};
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Button: styled.button`
|
||||||
|
width: ${({ width }) => width || "50px"};
|
||||||
|
min-width: ${({ minWidth }) => minWidth};
|
||||||
|
height: ${({ height }) => height || "38px"};
|
||||||
|
margin-right: ${({ marginRight }) => marginRight};
|
||||||
|
background-color: ${({ bgColor }) => bgColor || "tranceparent"};
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid ${({ borderColor }) => borderColor || "tranceparent"};
|
||||||
|
border-right: 1px solid
|
||||||
|
${({ borderColor }) => borderColor || "tranceparent"};
|
||||||
|
border-bottom: 1px solid
|
||||||
|
${({ borderColor }) => borderColor || "tranceparent"};
|
||||||
|
outline: none;
|
||||||
|
color: ${({ fontColor }) => fontColor || "black"};
|
||||||
|
font-size: ${({ fontSize }) => fontSize};
|
||||||
|
font-weight: 900;
|
||||||
|
`,
|
||||||
|
PossibleAmount: styled.span`
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Unit: styled.span`
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
`,
|
||||||
|
OrderBtnContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 50px;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderInfoAskBid = ({
|
||||||
|
theme,
|
||||||
|
selectedAskBidOrder,
|
||||||
|
coinSymbol,
|
||||||
|
orderPrice,
|
||||||
|
orderAmount,
|
||||||
|
orderTotalPrice,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const changePrice = useCallback(
|
||||||
|
(e) =>
|
||||||
|
dispatch(
|
||||||
|
changePriceAndTotalPrice(
|
||||||
|
parseInt(e.target.value.replace(/[^0-9-.]/g, ""))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
const changeAmount = useCallback(
|
||||||
|
(e) => {
|
||||||
|
dispatch(
|
||||||
|
changeAmountAndTotalPrice(e.target.value.replace(/[^0-9-.]/g, ""))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
const changeTotalPrice = useCallback(
|
||||||
|
(e) =>
|
||||||
|
dispatch(
|
||||||
|
changeTotalPriceAndAmount(
|
||||||
|
parseInt(e.target.value.replace(/[^0-9-.]/g, ""))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<St.OrderInfoContainer theme={theme}>
|
||||||
|
{selectedAskBidOrder !== "tradeList" ? (
|
||||||
|
<>
|
||||||
|
<St.OrderInfoDetailContainer>
|
||||||
|
<St.OrderInfoDetailTitle>주문가능</St.OrderInfoDetailTitle>
|
||||||
|
<St.PossibleAmount>
|
||||||
|
0
|
||||||
|
<St.Unit>
|
||||||
|
{selectedAskBidOrder === "bid" ? "KRW" : coinSymbol}
|
||||||
|
</St.Unit>
|
||||||
|
</St.PossibleAmount>
|
||||||
|
</St.OrderInfoDetailContainer>
|
||||||
|
<St.OrderInfoDetailContainer>
|
||||||
|
<St.OrderInfoDetailTitle>
|
||||||
|
{selectedAskBidOrder === "bid" ? "매수가격" : "매도가격"}
|
||||||
|
</St.OrderInfoDetailTitle>
|
||||||
|
<St.OrderInfoInputContainer>
|
||||||
|
<St.OrderInfoInput
|
||||||
|
onChange={changePrice}
|
||||||
|
value={orderPrice ? orderPrice.toLocaleString() : ""}
|
||||||
|
fontWeight={800}
|
||||||
|
placeholder={0}
|
||||||
|
/>
|
||||||
|
<St.Button
|
||||||
|
bgColor={theme.lightGray}
|
||||||
|
borderColor={theme.lightGray2}
|
||||||
|
fontColor={"#666"}
|
||||||
|
fontSize={"1.1rem"}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</St.Button>
|
||||||
|
<St.Button
|
||||||
|
bgColor={theme.lightGray}
|
||||||
|
borderColor={theme.lightGray2}
|
||||||
|
fontColor={"#666"}
|
||||||
|
fontSize={"1.1rem"}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</St.Button>
|
||||||
|
</St.OrderInfoInputContainer>
|
||||||
|
</St.OrderInfoDetailContainer>
|
||||||
|
<St.OrderInfoDetailContainer>
|
||||||
|
<St.OrderInfoDetailTitle>주문수량</St.OrderInfoDetailTitle>
|
||||||
|
<St.OrderInfoInput
|
||||||
|
onChange={changeAmount}
|
||||||
|
value={orderAmount ? orderAmount.toLocaleString() : ""}
|
||||||
|
placeholder={0}
|
||||||
|
/>
|
||||||
|
</St.OrderInfoDetailContainer>
|
||||||
|
<St.OrderInfoDetailContainer>
|
||||||
|
<St.OrderInfoDetailTitle>주문총액</St.OrderInfoDetailTitle>
|
||||||
|
<St.OrderInfoInput
|
||||||
|
onChange={changeTotalPrice}
|
||||||
|
value={orderTotalPrice ? orderTotalPrice.toLocaleString() : ""}
|
||||||
|
placeholder={0}
|
||||||
|
/>
|
||||||
|
</St.OrderInfoDetailContainer>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<OrderInfoTradeList theme={theme} />
|
||||||
|
)}
|
||||||
|
<St.OrderBtnContainer>
|
||||||
|
<St.Button
|
||||||
|
width={"30%"}
|
||||||
|
minWidth={"70px"}
|
||||||
|
marginRight={"5px"}
|
||||||
|
bgColor={theme.deepBlue}
|
||||||
|
fontSize={"0.9rem"}
|
||||||
|
fontColor={"white"}
|
||||||
|
>
|
||||||
|
회원가입
|
||||||
|
</St.Button>
|
||||||
|
<St.Button
|
||||||
|
width={"65%"}
|
||||||
|
bgColor={theme.priceDown}
|
||||||
|
fontSize={"0.9rem"}
|
||||||
|
fontColor={"white"}
|
||||||
|
>
|
||||||
|
로그인
|
||||||
|
</St.Button>
|
||||||
|
</St.OrderBtnContainer>
|
||||||
|
</St.OrderInfoContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(OrderInfoAskBid);
|
||||||
21
src/Components/Main/OrderInfoTradeList.js
Normal file
21
src/Components/Main/OrderInfoTradeList.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Container: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 212px;
|
||||||
|
background-color: white;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderInfoTradeList = ({ theme }) => {
|
||||||
|
return <St.Container>로그인 후 사용 가능합니다.</St.Container>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(OrderInfoTradeList);
|
||||||
124
src/Components/Main/Orderbook.js
Normal file
124
src/Components/Main/Orderbook.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import OrderbookItem from "./OrderbookItem";
|
||||||
|
import Loading from "../Global/Loading";
|
||||||
|
|
||||||
|
import withThemeData from "../../Container/withThemeData";
|
||||||
|
import withSelectedCoinPrice from "../../Container/withSelectedCoinPrice";
|
||||||
|
import withOrderbookData from "../../Container/withOrderbookData";
|
||||||
|
import withSelectedOption from "../../Container/withSelectedOption";
|
||||||
|
import withLoadingData from "../../Container/withLoadingData";
|
||||||
|
|
||||||
|
import isEqual from "react-fast-compare";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Container: styled.section`
|
||||||
|
width: 46%;
|
||||||
|
max-height: 722px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: white;
|
||||||
|
`,
|
||||||
|
HiddenH3: styled.h3`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
OrderUl: styled.ul`
|
||||||
|
width: 100%;
|
||||||
|
height: 722px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-color: ${({ theme }) => theme.middleGray};
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-base-color: transparent;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 5rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: ${({ theme }) => theme.middleGray};
|
||||||
|
border-radius: 5rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Orderbook = ({
|
||||||
|
theme,
|
||||||
|
// totalData,
|
||||||
|
askOrderbookData,
|
||||||
|
bidOrderbookData,
|
||||||
|
maxOrderSize,
|
||||||
|
beforeDayPrice,
|
||||||
|
selectedMarket,
|
||||||
|
isOrderbookLoading,
|
||||||
|
}) => {
|
||||||
|
const lastTradePrice = useSelector(
|
||||||
|
(state) =>
|
||||||
|
state.Coin.tradeList.data[selectedMarket] &&
|
||||||
|
state.Coin.tradeList.data[selectedMarket][0].trade_price
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<St.Container>
|
||||||
|
<St.HiddenH3>호가창</St.HiddenH3>
|
||||||
|
<St.OrderUl>
|
||||||
|
{isOrderbookLoading ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
askOrderbookData.map((orderbook, i) => {
|
||||||
|
return (
|
||||||
|
<OrderbookItem
|
||||||
|
theme={theme}
|
||||||
|
price={orderbook.askPrice}
|
||||||
|
size={orderbook.askSize}
|
||||||
|
maxOrderSize={maxOrderSize}
|
||||||
|
// key={`askOrder-${orderbook.askPrice}`}
|
||||||
|
key={`askOrder-${i}`}
|
||||||
|
type={"ask"}
|
||||||
|
changeRate24Hour={(
|
||||||
|
((orderbook.askPrice - beforeDayPrice) / beforeDayPrice) *
|
||||||
|
100
|
||||||
|
).toFixed(2)}
|
||||||
|
index={i}
|
||||||
|
outline={lastTradePrice === orderbook.askPrice}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
{isOrderbookLoading ||
|
||||||
|
bidOrderbookData.map((orderbook, i) => {
|
||||||
|
return (
|
||||||
|
<OrderbookItem
|
||||||
|
theme={theme}
|
||||||
|
price={orderbook.bidPrice}
|
||||||
|
size={orderbook.bidSize}
|
||||||
|
maxOrderSize={maxOrderSize}
|
||||||
|
// key={`bidOrder-${orderbook.bidPrice}`}
|
||||||
|
key={`bidOrder-${i}`}
|
||||||
|
type={"bid"}
|
||||||
|
changeRate24Hour={(
|
||||||
|
((orderbook.bidPrice - beforeDayPrice) / beforeDayPrice) *
|
||||||
|
100
|
||||||
|
).toFixed(2)}
|
||||||
|
index={i}
|
||||||
|
outline={lastTradePrice === orderbook.bidPrice}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</St.OrderUl>
|
||||||
|
</St.Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withOrderbookData()(
|
||||||
|
withSelectedCoinPrice()(
|
||||||
|
withSelectedOption()(
|
||||||
|
withLoadingData()(withThemeData()(React.memo(Orderbook, isEqual)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
64
src/Components/Main/OrderbookCoinInfo.js
Normal file
64
src/Components/Main/OrderbookCoinInfo.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
CandleInfoContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 33.3333%;
|
||||||
|
border: 1px solid gray;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
InfoContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 90%;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 5px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
InfoTxt: styled.span`
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
`,
|
||||||
|
|
||||||
|
InfoValue: styled.span`
|
||||||
|
display: block;
|
||||||
|
width: 50%;
|
||||||
|
text-align: right;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderbookCoinInfo = ({
|
||||||
|
volume24,
|
||||||
|
tradePrice24,
|
||||||
|
highestPrice52Week,
|
||||||
|
highestDate52Week,
|
||||||
|
lowestPrice52Week,
|
||||||
|
lowestDate52Week,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<St.CandleInfoContainer>
|
||||||
|
<St.InfoContainer>
|
||||||
|
<St.InfoTxt>거래량</St.InfoTxt>
|
||||||
|
<St.InfoValue>{volume24}</St.InfoValue>
|
||||||
|
</St.InfoContainer>
|
||||||
|
<St.InfoContainer>
|
||||||
|
<St.InfoTxt>거래대금</St.InfoTxt>
|
||||||
|
<St.InfoValue>{`${tradePrice24}백만`}</St.InfoValue>
|
||||||
|
</St.InfoContainer>
|
||||||
|
<St.InfoContainer>
|
||||||
|
<St.InfoTxt>52주 최고</St.InfoTxt>
|
||||||
|
<St.InfoValue>{`${highestPrice52Week} (${highestDate52Week})`}</St.InfoValue>
|
||||||
|
</St.InfoContainer>
|
||||||
|
<St.InfoContainer>
|
||||||
|
<St.InfoTxt>52주 최저</St.InfoTxt>
|
||||||
|
<St.InfoValue>{`${lowestPrice52Week} (${lowestDate52Week})`}</St.InfoValue>
|
||||||
|
</St.InfoContainer>
|
||||||
|
</St.CandleInfoContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(OrderbookCoinInfo);
|
||||||
169
src/Components/Main/OrderbookItem.js
Normal file
169
src/Components/Main/OrderbookItem.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { changePriceAndTotalPrice } from "../../Reducer/coinReducer";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
import isEqual from "react-fast-compare";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
OrderLi: styled.li`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
&:nth-last-child() {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
font-size: 0.8rem;
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
Btn: styled.button`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
|
||||||
|
OrderAmount: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
width: 50%;
|
||||||
|
height: 45px;
|
||||||
|
border: 1px solid ${({ borderColor }) => borderColor};
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
text-align: right;
|
||||||
|
`,
|
||||||
|
|
||||||
|
OrderAmountSize: styled.div`
|
||||||
|
position: absolute;
|
||||||
|
width: ${({ witdhSize }) => witdhSize};
|
||||||
|
left: 0;
|
||||||
|
height: 70%;
|
||||||
|
background-color: ${({ bgColor }) => bgColor};
|
||||||
|
`,
|
||||||
|
|
||||||
|
OrderPriceContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
width: 50%;
|
||||||
|
height: 45px;
|
||||||
|
border: 1px solid ${({ borderColor }) => borderColor};
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
text-align: right;
|
||||||
|
color: ${({ fontColor }) => fontColor};
|
||||||
|
background-color: ${({ bgColor }) => bgColor};
|
||||||
|
|
||||||
|
${({ outline }) =>
|
||||||
|
outline &&
|
||||||
|
css`
|
||||||
|
border: 2px solid black;
|
||||||
|
border-right: 3px solid black;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -5px;
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid black;
|
||||||
|
transform: rotate(225deg);
|
||||||
|
-ms-transform: rotate(225deg);
|
||||||
|
-webkit-transform: rotate(225deg);
|
||||||
|
-moz-transform: rotate(225deg);
|
||||||
|
-o-transform: rotate(225deg);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileM} {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
OrderPrice: styled.strong`
|
||||||
|
font-weight: 800;
|
||||||
|
`,
|
||||||
|
|
||||||
|
OrderPrcieRatio: styled.span`
|
||||||
|
padding-left: 13px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderbookItem = ({
|
||||||
|
theme,
|
||||||
|
price,
|
||||||
|
size,
|
||||||
|
maxOrderSize,
|
||||||
|
type,
|
||||||
|
changeRate24Hour,
|
||||||
|
index,
|
||||||
|
outline,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const scrollRef = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (index === 7 && type === "ask") {
|
||||||
|
const parentNode = scrollRef.current.parentNode;
|
||||||
|
const parentAbsoluteTop = window.pageYOffset + parentNode.offsetTop;
|
||||||
|
const absoluteTop = window.pageYOffset + scrollRef.current.offsetTop;
|
||||||
|
const relativeTop = absoluteTop - parentAbsoluteTop;
|
||||||
|
scrollRef.current.parentNode.scrollTop = relativeTop;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<St.OrderLi ref={scrollRef} theme={theme}>
|
||||||
|
<St.Btn
|
||||||
|
onClick={(_) => {
|
||||||
|
document.activeElement.blur();
|
||||||
|
dispatch(changePriceAndTotalPrice(price));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<St.OrderPriceContainer
|
||||||
|
theme={theme}
|
||||||
|
fontColor={
|
||||||
|
changeRate24Hour > 0
|
||||||
|
? theme.priceUp
|
||||||
|
: +changeRate24Hour < 0
|
||||||
|
? theme.priceDown
|
||||||
|
: "black"
|
||||||
|
}
|
||||||
|
borderColor={theme.lightGray}
|
||||||
|
bgColor={type === "ask" ? theme.skyBlue1 : theme.lightPink1}
|
||||||
|
// outline={lastTradePrice === price}
|
||||||
|
outline={outline}
|
||||||
|
>
|
||||||
|
<St.OrderPrice>{price.toLocaleString()}</St.OrderPrice>
|
||||||
|
<St.OrderPrcieRatio>{`${changeRate24Hour}%`}</St.OrderPrcieRatio>
|
||||||
|
</St.OrderPriceContainer>
|
||||||
|
<St.OrderAmount amountAlign={"left"} borderColor={theme.lightGray}>
|
||||||
|
{size}
|
||||||
|
<St.OrderAmountSize
|
||||||
|
witdhSize={`${Math.floor((size / maxOrderSize) * 100 - 10)}%`}
|
||||||
|
bgColor={type === "ask" ? theme.skyBlue2 : theme.lightPink2}
|
||||||
|
/>
|
||||||
|
</St.OrderAmount>
|
||||||
|
</St.Btn>
|
||||||
|
</St.OrderLi>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(OrderbookItem, isEqual);
|
||||||
122
src/Components/Main/TradeList.js
Normal file
122
src/Components/Main/TradeList.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Decimal from "decimal.js";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
|
import TradeListItem from "./TradeListItem";
|
||||||
|
import Loading from "../Global/Loading";
|
||||||
|
|
||||||
|
import withTradeListData from "../../Container/withTradeListData";
|
||||||
|
import withThemeData from "../../Container/withThemeData";
|
||||||
|
import withLoadingData from "../../Container/withLoadingData";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
Container: styled.article`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: white;
|
||||||
|
margin-top: 10px;
|
||||||
|
@media ${({ theme }) => theme.mobileM} {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HiddenH3: styled.h3`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
TradeListUL: styled.ul`
|
||||||
|
overflow-y: scroll;
|
||||||
|
scrollbar-color: ${(props) => props.scrollColor};
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-base-color: ${(props) => props.scrollColor};
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: ${(props) => props.scrollColor};
|
||||||
|
border-radius: 5rem;
|
||||||
|
}
|
||||||
|
height: 320px;
|
||||||
|
`,
|
||||||
|
TradeListTitle: styled.ul`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
height: 25px;
|
||||||
|
background-color: ${({ theme }) => theme.lightGray1};
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
TitleListItem: styled.li`
|
||||||
|
width: 20%;
|
||||||
|
|
||||||
|
min-width: 58px;
|
||||||
|
text-align: ${({ textAlign }) => textAlign || "center"};
|
||||||
|
@media ${({ theme, mobileSNone }) => (mobileSNone ? theme.mobileS : true)} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme, mobileMNone }) => (mobileMNone ? theme.mobileM : true)} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme, mobileSNone }) => mobileSNone || theme.mobileS} {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TradeList = ({ theme, selectedTradeListData, isTradeListLoading }) => {
|
||||||
|
return (
|
||||||
|
<St.Container>
|
||||||
|
<St.HiddenH3>실시간 체결내역</St.HiddenH3>
|
||||||
|
<St.TradeListTitle bgColor={theme.lightGray1}>
|
||||||
|
<St.TitleListItem mobileSNone={true} textAlign={"center"}>
|
||||||
|
체결시간
|
||||||
|
</St.TitleListItem>
|
||||||
|
<St.TitleListItem>체결가격</St.TitleListItem>
|
||||||
|
<St.TitleListItem>체결량</St.TitleListItem>
|
||||||
|
<St.TitleListItem mobileMNone={true} textAlign={"right"}>
|
||||||
|
체결금액
|
||||||
|
</St.TitleListItem>
|
||||||
|
</St.TradeListTitle>
|
||||||
|
<St.TradeListUL scrollColor={theme.middleGray}>
|
||||||
|
{isTradeListLoading || !selectedTradeListData ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
selectedTradeListData.map((tradeList, i) => {
|
||||||
|
const tradeAmount = new Decimal(tradeList.trade_volume) + "";
|
||||||
|
return (
|
||||||
|
<TradeListItem
|
||||||
|
theme={theme}
|
||||||
|
index={i}
|
||||||
|
// key={`tradeList-${tradeList.sequential_id}`}
|
||||||
|
key={`tradeList-${i}`}
|
||||||
|
date={moment(tradeList.timestamp).format("MM.DD")}
|
||||||
|
time={moment(tradeList.timestamp).format("HH:mm")}
|
||||||
|
tradePrice={tradeList.trade_price}
|
||||||
|
changePrice={tradeList.change_price}
|
||||||
|
tradeAmount={+tradeAmount}
|
||||||
|
askBid={tradeList.ask_bid}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</St.TradeListUL>
|
||||||
|
</St.Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTradeListData()(
|
||||||
|
withLoadingData()(withThemeData()(React.memo(TradeList)))
|
||||||
|
);
|
||||||
119
src/Components/Main/TradeListItem.js
Normal file
119
src/Components/Main/TradeListItem.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import isEqual from "react-fast-compare";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
TradeListLi: styled.li`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 25px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
background-color: ${({ bgColor }) => bgColor || "white"};
|
||||||
|
`,
|
||||||
|
|
||||||
|
Datetime: styled.div`
|
||||||
|
width: 20%;
|
||||||
|
text-align: center;
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Date: styled.span`
|
||||||
|
text-align: center;
|
||||||
|
`,
|
||||||
|
|
||||||
|
Time: styled.span`
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-left: 5px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
TradePrice: styled.span`
|
||||||
|
display: block;
|
||||||
|
width: 20%;
|
||||||
|
text-align: center;
|
||||||
|
color: ${({ fontColor }) => fontColor};
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
width: 50%;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
@media ${({ theme }) => theme.mobileM} {
|
||||||
|
/* width: 50%; */
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
TradeAmount: styled.span`
|
||||||
|
display: block;
|
||||||
|
width: 20%;
|
||||||
|
text-align: center;
|
||||||
|
color: ${({ fontColor }) => fontColor};
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
width: 50%;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileM} {
|
||||||
|
/* width: 50%; */
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
TradeKRW: styled.span`
|
||||||
|
display: block;
|
||||||
|
width: 20%;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileS} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.mobileM} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TradeListItem = ({
|
||||||
|
theme,
|
||||||
|
index,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
tradePrice,
|
||||||
|
changePrice,
|
||||||
|
tradeAmount,
|
||||||
|
askBid,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<St.TradeListLi
|
||||||
|
bgColor={index % 2 ? theme.lightGray1 : "white"}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
<St.Datetime>
|
||||||
|
<St.Date>{date}</St.Date>
|
||||||
|
<St.Time>{time}</St.Time>
|
||||||
|
</St.Datetime>
|
||||||
|
<St.TradePrice
|
||||||
|
fontColor={changePrice > 0 ? theme.priceUp : theme.priceDown}
|
||||||
|
>
|
||||||
|
{tradePrice.toLocaleString()}
|
||||||
|
</St.TradePrice>
|
||||||
|
<St.TradeAmount
|
||||||
|
theme={theme}
|
||||||
|
fontColor={askBid === "BID" ? theme.priceUp : theme.priceDown}
|
||||||
|
>
|
||||||
|
{tradeAmount.toFixed(5)}
|
||||||
|
</St.TradeAmount>
|
||||||
|
<St.TradeKRW theme={theme}>
|
||||||
|
{Math.floor(tradePrice * tradeAmount).toLocaleString()}
|
||||||
|
</St.TradeKRW>
|
||||||
|
</St.TradeListLi>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TradeListItem, isEqual);
|
||||||
33
src/Container/withLatestCoinData.js
Normal file
33
src/Container/withLatestCoinData.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withLatestCoinData = () => (OriginalComponent) => (props) => {
|
||||||
|
const coinListDatas = useSelector((state) => state.Coin.candle.data); // 코인들 데이터
|
||||||
|
|
||||||
|
const latestCoinData = {};
|
||||||
|
|
||||||
|
if (Object.keys(coinListDatas).length > 2) {
|
||||||
|
Object.keys(coinListDatas).forEach((marketName) => {
|
||||||
|
latestCoinData[marketName] = {};
|
||||||
|
latestCoinData[marketName].price =
|
||||||
|
coinListDatas[marketName].candles[
|
||||||
|
coinListDatas[marketName].candles.length - 1
|
||||||
|
].close;
|
||||||
|
|
||||||
|
latestCoinData[marketName].changeRate24Hour = (
|
||||||
|
Math.round(coinListDatas[marketName].changeRate24Hour * 10000) / 100
|
||||||
|
).toFixed(2);
|
||||||
|
|
||||||
|
latestCoinData[marketName].changePrice24Hour =
|
||||||
|
coinListDatas[marketName].changePrice24Hour;
|
||||||
|
|
||||||
|
latestCoinData[marketName].tradePrice24Hour = Math.floor(
|
||||||
|
coinListDatas[marketName].tradePrice24Hour / 1000000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <OriginalComponent {...props} latestCoinData={latestCoinData} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withLatestCoinData;
|
||||||
33
src/Container/withLoadingData.js
Normal file
33
src/Container/withLoadingData.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withLoadingData = () => (OriginalComponent) => (props) => {
|
||||||
|
const isCandleLoading = useSelector(
|
||||||
|
(state) => state.Loading["coin/GET_ONE_COIN_CANDLES"]
|
||||||
|
);
|
||||||
|
const isOrderbookLoading = useSelector(
|
||||||
|
(state) => state.Loading["coin/GET_INIT_ORDERBOOKS"]
|
||||||
|
);
|
||||||
|
const isTradeListLoading = useSelector(
|
||||||
|
(state) => state.Loading["coin/GET_ONE_COIN_TRADELISTS"]
|
||||||
|
);
|
||||||
|
const isInitCandleLoading = useSelector(
|
||||||
|
(state) => state.Loading["coin/GET_INIT_CANDLES"]
|
||||||
|
);
|
||||||
|
const isMarketNamesLoading = useSelector(
|
||||||
|
(state) => state.Loading["coin/GET_MARKET_NAMES"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
isCandleLoading={isCandleLoading}
|
||||||
|
isOrderbookLoading={isOrderbookLoading}
|
||||||
|
isTradeListLoading={isTradeListLoading}
|
||||||
|
isInitCandleLoading={isInitCandleLoading}
|
||||||
|
isMarketNamesLoading={isMarketNamesLoading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withLoadingData;
|
||||||
52
src/Container/withMarketNames.js
Normal file
52
src/Container/withMarketNames.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import * as Hangul from "hangul-js";
|
||||||
|
import { choHangul } from "../Lib/utils";
|
||||||
|
|
||||||
|
const withMarketNames = () => (OriginalComponent) => (props) => {
|
||||||
|
const marketNames = useSelector((state) => state.Coin.marketNames.data); // 코인 마켓 이름들(객체)
|
||||||
|
let marketNamesArr = Object.keys(marketNames); // 코인 마켓 이름 배열화
|
||||||
|
|
||||||
|
const coinListDatas = useSelector((state) => state.Coin.candle.data); // 코인들 데이터
|
||||||
|
const coinSearchInputData = useSelector((state) => state.Coin.searchCoin); // 검색한 코인 이름
|
||||||
|
|
||||||
|
// 데이터 받는 데 성공하면 필터링 및 정렬한다
|
||||||
|
if (Object.keys(coinListDatas).length > 1) {
|
||||||
|
// 검색 기준 필터링
|
||||||
|
marketNamesArr = marketNamesArr.filter(
|
||||||
|
(coin) =>
|
||||||
|
// 영어 검색
|
||||||
|
marketNames[coin].english
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(coinSearchInputData.toLowerCase()) ||
|
||||||
|
// 코인 심볼 검색
|
||||||
|
coin
|
||||||
|
.split("-")[1]
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(coinSearchInputData.toLowerCase()) ||
|
||||||
|
// 한글 검색
|
||||||
|
Hangul.disassembleToString(marketNames[coin].korean).includes(
|
||||||
|
Hangul.disassembleToString(coinSearchInputData)
|
||||||
|
) ||
|
||||||
|
// 초성 검색
|
||||||
|
choHangul(marketNames[coin].korean).includes(coinSearchInputData)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 정렬
|
||||||
|
marketNamesArr = marketNamesArr.sort((coin1, coin2) => {
|
||||||
|
return (
|
||||||
|
+coinListDatas[coin2].tradePrice24Hour -
|
||||||
|
+coinListDatas[coin1].tradePrice24Hour
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
marketNames={marketNames}
|
||||||
|
sortedMarketNames={marketNamesArr}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withMarketNames;
|
||||||
18
src/Container/withOHLCData.js
Normal file
18
src/Container/withOHLCData.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withOHLCData = () => (OriginalComponent) => () => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket); // 선택된 코인/마켓
|
||||||
|
const selectedCandles = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket].candles
|
||||||
|
); // 선택된 코인/마켓 캔들 정보
|
||||||
|
|
||||||
|
// return selectedCandles.length ? (
|
||||||
|
// <OriginalComponent data={selectedCandles} />
|
||||||
|
// ) : (
|
||||||
|
// <div className="center">Chart Loading</div>
|
||||||
|
// );
|
||||||
|
return <OriginalComponent data={selectedCandles} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withOHLCData;
|
||||||
59
src/Container/withOrderbookData.js
Normal file
59
src/Container/withOrderbookData.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withOrderbookData = () => (OriginalComponent) => (props) => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket);
|
||||||
|
const orderbook = useSelector(
|
||||||
|
(state) => state.Coin.orderbook.data[selectedMarket]
|
||||||
|
);
|
||||||
|
|
||||||
|
let totalData;
|
||||||
|
let bidOrderbookData;
|
||||||
|
let askOrderbookData;
|
||||||
|
let orderbookData;
|
||||||
|
let maxOrderSize = 0;
|
||||||
|
|
||||||
|
if (orderbook) {
|
||||||
|
totalData = {
|
||||||
|
totalBidSize: orderbook.total_bid_size,
|
||||||
|
totalAskSize: orderbook.total_ask_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
bidOrderbookData = [];
|
||||||
|
askOrderbookData = [];
|
||||||
|
|
||||||
|
// let maxOrderSize = 0;
|
||||||
|
// 호가 데이터 분리 정렬
|
||||||
|
orderbook.orderbook_units.forEach((orderbook, i) => {
|
||||||
|
const bidSize = orderbook.bid_size.toFixed(3);
|
||||||
|
const askSize = orderbook.ask_size.toFixed(3);
|
||||||
|
|
||||||
|
bidOrderbookData.push({
|
||||||
|
bidPrice: orderbook.bid_price,
|
||||||
|
bidSize: orderbook.bid_size.toFixed(3),
|
||||||
|
});
|
||||||
|
askOrderbookData.push({
|
||||||
|
askPrice: orderbook.ask_price,
|
||||||
|
askSize: orderbook.ask_size.toFixed(3),
|
||||||
|
});
|
||||||
|
maxOrderSize = Math.max(maxOrderSize, bidSize, askSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
orderbookData = [...askOrderbookData, ...bidOrderbookData];
|
||||||
|
// 매도 호가창은 가격 내림차순으로 정렬해줌 (매수는 원래 가격 내림차순임)
|
||||||
|
askOrderbookData.sort((book1, book2) => +book2.askPrice - +book1.askPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
totalData={totalData || []}
|
||||||
|
orderbookData={orderbookData || []}
|
||||||
|
bidOrderbookData={bidOrderbookData || []}
|
||||||
|
askOrderbookData={askOrderbookData || []}
|
||||||
|
maxOrderSize={maxOrderSize || 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withOrderbookData;
|
||||||
28
src/Container/withSelectedCoinName.js
Normal file
28
src/Container/withSelectedCoinName.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withSelectedCoinName = () => (OriginalComponent) => (props) => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket);
|
||||||
|
const coinNameKor = useSelector(
|
||||||
|
(state) => state.Coin.marketNames.data[selectedMarket].korean
|
||||||
|
);
|
||||||
|
const coinNameEng = useSelector(
|
||||||
|
(state) => state.Coin.marketNames.data[selectedMarket].english
|
||||||
|
);
|
||||||
|
|
||||||
|
const splitedName = selectedMarket.split("-");
|
||||||
|
const coinSymbol = splitedName[1];
|
||||||
|
const coinNameAndMarketEng = splitedName[1] + "/" + splitedName[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
coinNameKor={coinNameKor}
|
||||||
|
coinNameEng={coinNameEng}
|
||||||
|
coinSymbol={coinSymbol}
|
||||||
|
coinNameAndMarketEng={coinNameAndMarketEng}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSelectedCoinName;
|
||||||
76
src/Container/withSelectedCoinPrice.js
Normal file
76
src/Container/withSelectedCoinPrice.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withSelectedCoinPrice = () => (OriginalComponent) => (props) => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket);
|
||||||
|
const selectedCoinData = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 24시간 고가 저가
|
||||||
|
const highestPrice24Hour = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket]["highestPrice24Hour"]
|
||||||
|
);
|
||||||
|
const lowestPrice24Hour = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket]["lowestPrice24Hour"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 52주 고가 저가
|
||||||
|
const highestPrice52Week = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket].highestPrice52Week
|
||||||
|
);
|
||||||
|
const highestDate52Week = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket].highestDate52Week
|
||||||
|
);
|
||||||
|
const lowestPrice52Week = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket].lowestPrice52Week
|
||||||
|
);
|
||||||
|
const lowestDate52Week = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket].lowestDate52Week
|
||||||
|
);
|
||||||
|
|
||||||
|
// 24시간 거래대금, 거래량
|
||||||
|
const tradePrice24Hour = Math.floor(selectedCoinData.tradePrice24Hour);
|
||||||
|
const volume24Hour = Math.floor(selectedCoinData.volume24Hour);
|
||||||
|
|
||||||
|
// 24시간 가격 변화율, 변화량
|
||||||
|
const changeRate24Hour =
|
||||||
|
Math.round(selectedCoinData.changeRate24Hour * 10000) / 100;
|
||||||
|
const changePrice24Hour = selectedCoinData.changePrice24Hour
|
||||||
|
? selectedCoinData.changePrice24Hour
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 전일, 당일 가격
|
||||||
|
const selecteCoinCadnles = useSelector(
|
||||||
|
(state) => state.Coin.candle.data[selectedMarket].candles
|
||||||
|
);
|
||||||
|
const lastCandleIndex = selecteCoinCadnles.length - 1;
|
||||||
|
|
||||||
|
const beforeDayPrice = selecteCoinCadnles.length
|
||||||
|
? selecteCoinCadnles[lastCandleIndex].close - changePrice24Hour
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const price = selecteCoinCadnles.length
|
||||||
|
? selecteCoinCadnles[selecteCoinCadnles.length - 1].close
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
highestPrice52Week={highestPrice52Week}
|
||||||
|
highestDate52Week={highestDate52Week}
|
||||||
|
lowestPrice52Week={lowestPrice52Week}
|
||||||
|
lowestDate52Week={lowestDate52Week}
|
||||||
|
highestPrice24Hour={highestPrice24Hour}
|
||||||
|
lowestPrice24Hour={lowestPrice24Hour}
|
||||||
|
tradePrice24Hour={tradePrice24Hour}
|
||||||
|
volume24Hour={volume24Hour}
|
||||||
|
changeRate24Hour={changeRate24Hour}
|
||||||
|
changePrice24Hour={changePrice24Hour}
|
||||||
|
beforeDayPrice={beforeDayPrice}
|
||||||
|
price={price}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSelectedCoinPrice;
|
||||||
33
src/Container/withSelectedOption.js
Normal file
33
src/Container/withSelectedOption.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withSelectedOption = () => (OriginalComponent) => (props) => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket);
|
||||||
|
const selectedTimeType = useSelector((state) => state.Coin.selectedTimeType);
|
||||||
|
const selectedTimeCount = useSelector(
|
||||||
|
(state) => state.Coin.selectedTimeCount
|
||||||
|
);
|
||||||
|
const selectedAskBidOrder = useSelector(
|
||||||
|
(state) => state.Coin.selectedAskBidOrder
|
||||||
|
);
|
||||||
|
const searchCoin = useSelector((state) => state.Coin.searchCoin);
|
||||||
|
const orderPrice = useSelector((state) => state.Coin.orderPrice);
|
||||||
|
const orderAmount = useSelector((state) => state.Coin.orderAmount);
|
||||||
|
const orderTotalPrice = useSelector((state) => state.Coin.orderTotalPrice);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
selectedMarket={selectedMarket}
|
||||||
|
selectedTimeType={selectedTimeType}
|
||||||
|
selectedTimeCount={selectedTimeCount}
|
||||||
|
selectedAskBidOrder={selectedAskBidOrder}
|
||||||
|
searchCoinInput={searchCoin}
|
||||||
|
orderPrice={orderPrice}
|
||||||
|
orderAmount={orderAmount}
|
||||||
|
orderTotalPrice={orderTotalPrice}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSelectedOption;
|
||||||
29
src/Container/withSize.js
Normal file
29
src/Container/withSize.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { throttle } from "lodash";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const withSize = () => (OriginalComponent) => (props) => {
|
||||||
|
const [widthSize, setWidthSize] = useState(window.innerWidth);
|
||||||
|
const [heightSize, setHeightSize] = useState(window.innerHeight);
|
||||||
|
|
||||||
|
const handleSize = useCallback(() => {
|
||||||
|
setWidthSize(window.innerWidth);
|
||||||
|
setHeightSize(window.innerHeight);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("resize", throttle(handleSize, 200));
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleSize);
|
||||||
|
};
|
||||||
|
}, [handleSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
widthSize={widthSize}
|
||||||
|
heightSize={heightSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSize;
|
||||||
9
src/Container/withThemeData.js
Normal file
9
src/Container/withThemeData.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { ThemeContext } from "styled-components";
|
||||||
|
|
||||||
|
const withThemeData = () => (OriginalComponent) => (props) => {
|
||||||
|
const theme = useContext(ThemeContext); // 테마 정보
|
||||||
|
return <OriginalComponent {...props} theme={theme} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withThemeData;
|
||||||
18
src/Container/withTradeListData.js
Normal file
18
src/Container/withTradeListData.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const withTradeListData = () => (OriginalComponent) => (props) => {
|
||||||
|
const selectedMarket = useSelector((state) => state.Coin.selectedMarket);
|
||||||
|
const selectedTradeListData = useSelector(
|
||||||
|
(state) => state.Coin.tradeList.data[selectedMarket]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OriginalComponent
|
||||||
|
{...props}
|
||||||
|
selectedTradeListData={selectedTradeListData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTradeListData;
|
||||||
290
src/Lib/asyncUtil.js
Normal file
290
src/Lib/asyncUtil.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import { w3cwebsocket as W3CWebSocket } from "websocket";
|
||||||
|
import { call, put, select, flush, delay } from "redux-saga/effects";
|
||||||
|
import { startLoading, finishLoading } from "../Reducer/loadingReducer";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
import { buffers, eventChannel, END } from "redux-saga";
|
||||||
|
import encoding from "text-encoding";
|
||||||
|
|
||||||
|
// 캔들용 사가
|
||||||
|
const createRequestSaga = (type, api, dataMaker) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
const ERROR = `${type}_ERROR`;
|
||||||
|
|
||||||
|
return function* (action = {}) {
|
||||||
|
yield put(startLoading(type));
|
||||||
|
try {
|
||||||
|
const res = yield call(api, action.payload);
|
||||||
|
const state = yield select();
|
||||||
|
|
||||||
|
yield put({ type: SUCCESS, payload: dataMaker(res.data, state) });
|
||||||
|
yield put(finishLoading(type));
|
||||||
|
} catch (e) {
|
||||||
|
yield put({ type: ERROR, payload: e });
|
||||||
|
yield put(finishLoading(type));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 선택 옵션 변경용 사가
|
||||||
|
const createChangeOptionSaga = (type) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
|
||||||
|
return function* (action = {}) {
|
||||||
|
yield put({ type: SUCCESS, payload: action.payload });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 웹소켓 연결용 Thunk
|
||||||
|
const createConnectSocketThunk = (type, connectType, dataMaker) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
const ERROR = `${type}_ERROR`;
|
||||||
|
|
||||||
|
return (action = {}) => (dispatch, getState) => {
|
||||||
|
const client = new W3CWebSocket("wss://api.upbit.com/websocket/v1");
|
||||||
|
client.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
client.onopen = () => {
|
||||||
|
client.send(
|
||||||
|
JSON.stringify([
|
||||||
|
{ ticket: "downbit-clone" },
|
||||||
|
{ type: connectType, codes: action.payload },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.onmessage = (evt) => {
|
||||||
|
const enc = new encoding.TextDecoder("utf-8");
|
||||||
|
const arr = new Uint8Array(evt.data);
|
||||||
|
const data = JSON.parse(enc.decode(arr));
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
dispatch({ type: SUCCESS, payload: dataMaker(data, state) });
|
||||||
|
};
|
||||||
|
|
||||||
|
client.onerror = (e) => {
|
||||||
|
dispatch({ type: ERROR, payload: e });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 웹소켓 연결용 Thunk
|
||||||
|
const createConnectSocketThrottleThunk = (type, connectType, dataMaker) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
const ERROR = `${type}_ERROR`;
|
||||||
|
const throttleDispatch = throttle((dispatch, state, data) => {
|
||||||
|
dispatch({ type: SUCCESS, payload: dataMaker(data, state) });
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return (action = {}) => (dispatch, getState) => {
|
||||||
|
const client = new W3CWebSocket("wss://api.upbit.com/websocket/v1");
|
||||||
|
client.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
client.onopen = () => {
|
||||||
|
client.send(
|
||||||
|
JSON.stringify([
|
||||||
|
{ ticket: "downbit-clone" },
|
||||||
|
{ type: connectType, codes: action.payload },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.onmessage = (evt) => {
|
||||||
|
const enc = new encoding.TextDecoder("utf-8");
|
||||||
|
const arr = new Uint8Array(evt.data);
|
||||||
|
const data = JSON.parse(enc.decode(arr));
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
// dispatch({ type: SUCCESS, payload: dataMaker(data, state) });
|
||||||
|
throttleDispatch(dispatch, state, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.onerror = (e) => {
|
||||||
|
dispatch({ type: ERROR, payload: e });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 소켓 만들기
|
||||||
|
const createSocket = () => {
|
||||||
|
const client = new W3CWebSocket("wss://api.upbit.com/websocket/v1");
|
||||||
|
client.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 소켓 연결용
|
||||||
|
const connectSocekt = (socket, connectType, action, buffer) => {
|
||||||
|
return eventChannel((emit) => {
|
||||||
|
socket.onopen = () => {
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify([
|
||||||
|
{ ticket: "downbit-clone" },
|
||||||
|
{ type: connectType, codes: action.payload },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (evt) => {
|
||||||
|
const enc = new encoding.TextDecoder("utf-8");
|
||||||
|
// const arr = new Uint8Array(evt.data);
|
||||||
|
const data = JSON.parse(enc.decode(evt.data));
|
||||||
|
|
||||||
|
emit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (evt) => {
|
||||||
|
emit(evt);
|
||||||
|
emit(END);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = () => {
|
||||||
|
socket.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, buffer || buffers.none());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 웹소켓 연결용 사가
|
||||||
|
const createConnectSocketSaga = (type, connectType, dataMaker) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
const ERROR = `${type}_ERROR`;
|
||||||
|
|
||||||
|
return function* (action = {}) {
|
||||||
|
const client = yield call(createSocket);
|
||||||
|
const clientChannel = yield call(
|
||||||
|
connectSocekt,
|
||||||
|
client,
|
||||||
|
connectType,
|
||||||
|
action,
|
||||||
|
buffers.expanding(500)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const datas = yield flush(clientChannel); // 버퍼 데이터 가져오기
|
||||||
|
const state = yield select();
|
||||||
|
|
||||||
|
if (datas.length) {
|
||||||
|
const sortedObj = {};
|
||||||
|
datas.forEach((data) => {
|
||||||
|
if (sortedObj[data.code]) {
|
||||||
|
// 버퍼에 있는 데이터중 시간이 가장 최근인 데이터만 남김
|
||||||
|
sortedObj[data.code] =
|
||||||
|
sortedObj[data.code].timestamp > data.timestamp
|
||||||
|
? sortedObj[data.code]
|
||||||
|
: data;
|
||||||
|
} else {
|
||||||
|
sortedObj[data.code] = data; // 새로운 데이터면 그냥 넣음
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedData = Object.keys(sortedObj).map(
|
||||||
|
(data) => sortedObj[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
yield put({ type: SUCCESS, payload: dataMaker(sortedData, state) });
|
||||||
|
}
|
||||||
|
yield delay(500); // 500ms 동안 대기
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
yield put({ type: ERROR, payload: e });
|
||||||
|
} finally {
|
||||||
|
clientChannel.close(); // emit(END) 접근시 소켓 닫기
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducerUtils = {
|
||||||
|
success: (state, payload, key) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[key]: {
|
||||||
|
data: payload,
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
error: (state, error, key) => ({
|
||||||
|
...state,
|
||||||
|
[key]: {
|
||||||
|
...state[key],
|
||||||
|
error: error,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestActions = (type, key) => {
|
||||||
|
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
|
||||||
|
return (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SUCCESS:
|
||||||
|
return reducerUtils.success(state, action.payload, key);
|
||||||
|
case ERROR:
|
||||||
|
return reducerUtils.error(state, action.payload, key);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestInitActions = (type, key) => {
|
||||||
|
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
|
||||||
|
return (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
candleDay: {
|
||||||
|
data: action.payload,
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
[key]: {
|
||||||
|
data: action.payload,
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
candleDay: {
|
||||||
|
...state.candleDay,
|
||||||
|
error: action.payload,
|
||||||
|
},
|
||||||
|
[key]: {
|
||||||
|
...state[key],
|
||||||
|
error: action.payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeOptionActions = (type, key) => {
|
||||||
|
const SUCCESS = `${type}_SUCCESS`;
|
||||||
|
return (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[key]: action.payload,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
createRequestSaga,
|
||||||
|
createConnectSocketThunk,
|
||||||
|
createConnectSocketThrottleThunk,
|
||||||
|
createConnectSocketSaga,
|
||||||
|
createChangeOptionSaga,
|
||||||
|
requestActions,
|
||||||
|
requestInitActions,
|
||||||
|
changeOptionActions,
|
||||||
|
};
|
||||||
529
src/Lib/utils.js
Normal file
529
src/Lib/utils.js
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
import moment from "moment-timezone";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
|
||||||
|
const dateFormat = d3.timeParse("%Y-%m-%d %H:%M");
|
||||||
|
|
||||||
|
const timestampToDatetime = (timeType, timeCount, timestamp) => {
|
||||||
|
switch (timeType) {
|
||||||
|
case "minute":
|
||||||
|
case "minutes":
|
||||||
|
return (
|
||||||
|
moment(timestamp)
|
||||||
|
.minute(
|
||||||
|
Math.floor(moment(timestamp).minute() / timeCount) * timeCount
|
||||||
|
)
|
||||||
|
.second(0)
|
||||||
|
// .tz("Asia/Seoul")
|
||||||
|
.format("YYYY-MM-DD HH:mm")
|
||||||
|
);
|
||||||
|
case "hour":
|
||||||
|
case "hours":
|
||||||
|
return (
|
||||||
|
moment(timestamp)
|
||||||
|
.hour(Math.floor(moment(timestamp).hour() / timeCount) * timeCount)
|
||||||
|
.minute(0)
|
||||||
|
.second(0)
|
||||||
|
// .tz("Asia/Seoul")
|
||||||
|
.format("YYYY-MM-DD HH:mm")
|
||||||
|
);
|
||||||
|
case "day":
|
||||||
|
case "days":
|
||||||
|
return moment(timestamp)
|
||||||
|
.hour(9)
|
||||||
|
.minute(0)
|
||||||
|
.second(0)
|
||||||
|
.format("YYYY-MM-DD HH:mm");
|
||||||
|
case "week":
|
||||||
|
case "weeks":
|
||||||
|
return moment(timestamp)
|
||||||
|
.hour(0)
|
||||||
|
.minute(0)
|
||||||
|
.second(0)
|
||||||
|
.format("YYYY-MM-DD HH:mm");
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const candleDataUtils = {
|
||||||
|
init: (candles, state) => {
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
candles.forEach((candle) => {
|
||||||
|
data[candle.market] = {};
|
||||||
|
data[candle.market]["candles"] = [];
|
||||||
|
data[candle.market]["candles"].push({
|
||||||
|
date: dateFormat(
|
||||||
|
timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
)
|
||||||
|
),
|
||||||
|
datetime: timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
),
|
||||||
|
timestamp: candle.timestamp,
|
||||||
|
open: candle.opening_price,
|
||||||
|
high: candle.high_price,
|
||||||
|
low: candle.low_price,
|
||||||
|
close: candle.trade_price,
|
||||||
|
volume: candle.acc_trade_volume,
|
||||||
|
tradePrice: candle.acc_trade_price,
|
||||||
|
});
|
||||||
|
data[candle.market]["tradePrice24Hour"] = candle.acc_trade_price_24h;
|
||||||
|
data[candle.market]["volume24Hour"] = candle.acc_trade_volume_24h;
|
||||||
|
data[candle.market]["changeRate24Hour"] = candle.signed_change_rate;
|
||||||
|
data[candle.market]["changePrice24Hour"] = candle.signed_change_price;
|
||||||
|
data[candle.market]["highestPrice24Hour"] = candle.high_price;
|
||||||
|
data[candle.market]["lowestPrice24Hour"] = candle.low_price;
|
||||||
|
data[candle.market]["highestPrice52Week"] = candle.highest_52_week_price;
|
||||||
|
data[candle.market]["highestDate52Week"] = candle.highest_52_week_date;
|
||||||
|
data[candle.market]["lowestPrice52Week"] = candle.lowest_52_week_price;
|
||||||
|
data[candle.market]["lowestDate52Week"] = candle.lowest_52_week_date;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: (candle, state) => {
|
||||||
|
const candleStateDatas = state.Coin.candle.data;
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
|
||||||
|
const coinMarket = candle.code;
|
||||||
|
|
||||||
|
const targetCandles = candleStateDatas[coinMarket].candles;
|
||||||
|
const lastCandle = targetCandles.slice(-1)[0];
|
||||||
|
|
||||||
|
const date = dateFormat(
|
||||||
|
timestampToDatetime(selectedTimeType, selectedTimeCount, candle.timestamp)
|
||||||
|
);
|
||||||
|
const datetime = timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
);
|
||||||
|
const open = lastCandle.open;
|
||||||
|
const high =
|
||||||
|
candle.trade_price > lastCandle.high
|
||||||
|
? candle.trade_price
|
||||||
|
: lastCandle.high;
|
||||||
|
const low =
|
||||||
|
candle.trade_price < lastCandle.low ? candle.trade_price : lastCandle.low;
|
||||||
|
const close = candle.trade_price;
|
||||||
|
|
||||||
|
const highestPrice24Hour = candleStateDatas[coinMarket].highestPrice24Hour;
|
||||||
|
const lowestPrice24Hour = candleStateDatas[coinMarket].lowestPrice24Hour;
|
||||||
|
|
||||||
|
const needUpdate = targetCandles.find(
|
||||||
|
(candle) => candle.datetime === datetime
|
||||||
|
);
|
||||||
|
const dateChanged =
|
||||||
|
d3.timeParse("YYYY-MM-DD")(lastCandle.date) !==
|
||||||
|
d3.timeParse("YYYY-MM-DD")(datetime);
|
||||||
|
|
||||||
|
const newData = { ...candleStateDatas }; // 원본 데이터 보장
|
||||||
|
if (needUpdate) {
|
||||||
|
const volume = needUpdate.volume + candle.trade_volume;
|
||||||
|
const tradePrice = needUpdate.tradePrice + candle.trade_price;
|
||||||
|
const updatedCandles = [...targetCandles];
|
||||||
|
updatedCandles.pop();
|
||||||
|
updatedCandles.push({
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
timestamp: candle.timestamp,
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
close,
|
||||||
|
volume,
|
||||||
|
tradePrice,
|
||||||
|
});
|
||||||
|
|
||||||
|
newData[coinMarket]["candles"] = updatedCandles;
|
||||||
|
newData[coinMarket]["tradePrice24Hour"] = candle.acc_trade_price_24h;
|
||||||
|
newData[coinMarket]["volume24Hour"] = candle.acc_trade_volume_24h;
|
||||||
|
newData[coinMarket]["changeRate24Hour"] = candle.signed_change_rate;
|
||||||
|
newData[coinMarket]["changePrice24Hour"] = candle.signed_change_price;
|
||||||
|
newData[coinMarket]["highestPrice24Hour"] =
|
||||||
|
high > highestPrice24Hour ? high : highestPrice24Hour;
|
||||||
|
newData[coinMarket]["lowestPrice24Hour"] =
|
||||||
|
low < lowestPrice24Hour ? low : lowestPrice24Hour;
|
||||||
|
newData[coinMarket]["highestPrice52Week"] = candle.highest_52_week_price;
|
||||||
|
newData[coinMarket]["highestDate52Week"] = candle.highest_52_week_date;
|
||||||
|
newData[coinMarket]["lowestPrice52Week"] = candle.lowest_52_week_price;
|
||||||
|
newData[coinMarket]["lowestDate52Week"] = candle.lowest_52_week_date;
|
||||||
|
} else {
|
||||||
|
const volume = candle.trade_volume;
|
||||||
|
const tradePrice = candle.trade_price;
|
||||||
|
|
||||||
|
newData[coinMarket]["candles"] = [
|
||||||
|
...targetCandles,
|
||||||
|
{
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
timestamp: candle.timestamp,
|
||||||
|
dateKst: candle.trade_date_kst,
|
||||||
|
timeKst: candle.trade_time_kst,
|
||||||
|
open: close,
|
||||||
|
high: close,
|
||||||
|
low: close,
|
||||||
|
close,
|
||||||
|
volume,
|
||||||
|
tradePrice,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
newData[coinMarket]["tradePrice24Hour"] = candle.acc_trade_price_24h;
|
||||||
|
newData[coinMarket]["volume24Hour"] = candle.acc_trade_volume_24h;
|
||||||
|
newData[coinMarket]["changeRate24Hour"] = candle.signed_change_rate;
|
||||||
|
newData[coinMarket]["changePrice24Hour"] = candle.signed_change_price;
|
||||||
|
newData[coinMarket]["highestPrice24Hour"] = dateChanged // 날짜가 바뀌지 않았을때만 고점 갱신기록, 날짜 바뀌면 지금 고점 기록
|
||||||
|
? high
|
||||||
|
: high > highestPrice24Hour
|
||||||
|
? high
|
||||||
|
: highestPrice24Hour;
|
||||||
|
newData[coinMarket]["lowestPrice24Hour"] = dateChanged
|
||||||
|
? low
|
||||||
|
: low < lowestPrice24Hour
|
||||||
|
? low
|
||||||
|
: lowestPrice24Hour;
|
||||||
|
newData[coinMarket]["highestPrice52Week"] = candle.highest_52_week_price;
|
||||||
|
newData[coinMarket]["highestDate52Week"] = candle.highest_52_week_date;
|
||||||
|
newData[coinMarket]["lowestPrice52Week"] = candle.lowest_52_week_price;
|
||||||
|
newData[coinMarket]["lowestDate52Week"] = candle.lowest_52_week_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
},
|
||||||
|
updates: (candles, state) => {
|
||||||
|
const candleStateDatas = state.Coin.candle.data;
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
|
||||||
|
const newData = { ...candleStateDatas }; // 원본 데이터 보장
|
||||||
|
|
||||||
|
candles.forEach((candle) => {
|
||||||
|
const coinMarket = candle.code;
|
||||||
|
|
||||||
|
const targetCandles = candleStateDatas[coinMarket].candles;
|
||||||
|
const lastCandle = targetCandles.slice(-1)[0];
|
||||||
|
|
||||||
|
const date = dateFormat(
|
||||||
|
timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const datetime = timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
);
|
||||||
|
const open = lastCandle.open;
|
||||||
|
const high =
|
||||||
|
candle.trade_price > lastCandle.high
|
||||||
|
? candle.trade_price
|
||||||
|
: lastCandle.high;
|
||||||
|
const low =
|
||||||
|
candle.trade_price < lastCandle.low
|
||||||
|
? candle.trade_price
|
||||||
|
: lastCandle.low;
|
||||||
|
const close = candle.trade_price;
|
||||||
|
|
||||||
|
const highestPrice24Hour =
|
||||||
|
candleStateDatas[coinMarket].highestPrice24Hour;
|
||||||
|
const lowestPrice24Hour = candleStateDatas[coinMarket].lowestPrice24Hour;
|
||||||
|
|
||||||
|
const needUpdate = targetCandles.find(
|
||||||
|
(candle) => candle.datetime === datetime
|
||||||
|
);
|
||||||
|
const dateChanged =
|
||||||
|
d3.timeParse("YYYY-MM-DD")(lastCandle.date) !==
|
||||||
|
d3.timeParse("YYYY-MM-DD")(datetime);
|
||||||
|
|
||||||
|
if (needUpdate) {
|
||||||
|
const volume = needUpdate.volume + candle.trade_volume;
|
||||||
|
const tradePrice = needUpdate.tradePrice + candle.trade_price;
|
||||||
|
const updatedCandles = [...targetCandles];
|
||||||
|
updatedCandles.pop();
|
||||||
|
updatedCandles.push({
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
timestamp: candle.timestamp,
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
close,
|
||||||
|
volume,
|
||||||
|
tradePrice,
|
||||||
|
});
|
||||||
|
|
||||||
|
newData[coinMarket]["candles"] = updatedCandles;
|
||||||
|
newData[coinMarket]["tradePrice24Hour"] = candle.acc_trade_price_24h;
|
||||||
|
newData[coinMarket]["volume24Hour"] = candle.acc_trade_volume_24h;
|
||||||
|
newData[coinMarket]["changeRate24Hour"] = candle.signed_change_rate;
|
||||||
|
newData[coinMarket]["changePrice24Hour"] = candle.signed_change_price;
|
||||||
|
newData[coinMarket]["highestPrice24Hour"] =
|
||||||
|
high > highestPrice24Hour ? high : highestPrice24Hour;
|
||||||
|
newData[coinMarket]["lowestPrice24Hour"] =
|
||||||
|
low < lowestPrice24Hour ? low : lowestPrice24Hour;
|
||||||
|
newData[coinMarket]["highestPrice52Week"] =
|
||||||
|
candle.highest_52_week_price;
|
||||||
|
newData[coinMarket]["highestDate52Week"] = candle.highest_52_week_date;
|
||||||
|
newData[coinMarket]["lowestPrice52Week"] = candle.lowest_52_week_price;
|
||||||
|
newData[coinMarket]["lowestDate52Week"] = candle.lowest_52_week_date;
|
||||||
|
} else {
|
||||||
|
const volume = candle.trade_volume;
|
||||||
|
const tradePrice = candle.trade_price;
|
||||||
|
|
||||||
|
newData[coinMarket]["candles"] = [
|
||||||
|
...targetCandles,
|
||||||
|
{
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
timestamp: candle.timestamp,
|
||||||
|
dateKst: candle.trade_date_kst,
|
||||||
|
timeKst: candle.trade_time_kst,
|
||||||
|
open: close,
|
||||||
|
high: close,
|
||||||
|
low: close,
|
||||||
|
close,
|
||||||
|
volume,
|
||||||
|
tradePrice,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
newData[coinMarket]["tradePrice24Hour"] = candle.acc_trade_price_24h;
|
||||||
|
newData[coinMarket]["volume24Hour"] = candle.acc_trade_volume_24h;
|
||||||
|
newData[coinMarket]["changeRate24Hour"] = candle.signed_change_rate;
|
||||||
|
newData[coinMarket]["changePrice24Hour"] = candle.signed_change_price;
|
||||||
|
newData[coinMarket]["highestPrice24Hour"] = dateChanged // 날짜가 바뀌지 않았을때만 고점 갱신기록, 날짜 바뀌면 지금 고점 기록
|
||||||
|
? high
|
||||||
|
: high > highestPrice24Hour
|
||||||
|
? high
|
||||||
|
: highestPrice24Hour;
|
||||||
|
newData[coinMarket]["lowestPrice24Hour"] = dateChanged
|
||||||
|
? low
|
||||||
|
: low < lowestPrice24Hour
|
||||||
|
? low
|
||||||
|
: lowestPrice24Hour;
|
||||||
|
newData[coinMarket]["highestPrice52Week"] =
|
||||||
|
candle.highest_52_week_price;
|
||||||
|
newData[coinMarket]["highestDate52Week"] = candle.highest_52_week_date;
|
||||||
|
newData[coinMarket]["lowestPrice52Week"] = candle.lowest_52_week_price;
|
||||||
|
newData[coinMarket]["lowestDate52Week"] = candle.lowest_52_week_date;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
},
|
||||||
|
oneCoin: (candles, state) => {
|
||||||
|
const candleStateData = state.Coin.candle.data;
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
const market = candles[0].market;
|
||||||
|
|
||||||
|
const newCandles = candles.map((candle) => {
|
||||||
|
return {
|
||||||
|
date: dateFormat(
|
||||||
|
timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
)
|
||||||
|
),
|
||||||
|
datetime: timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
),
|
||||||
|
timestamp: candle.timestamp,
|
||||||
|
open: candle.opening_price,
|
||||||
|
high: candle.high_price,
|
||||||
|
low: candle.low_price,
|
||||||
|
close: candle.trade_price,
|
||||||
|
volume: candle.candle_acc_trade_volume,
|
||||||
|
tradePrice: candle.candle_acc_trade_price,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const newData = {
|
||||||
|
...candleStateData,
|
||||||
|
[market]: {
|
||||||
|
...candleStateData[market],
|
||||||
|
candles: newCandles,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
},
|
||||||
|
add: (candles, state) => {
|
||||||
|
const candleStateData = state.Coin.candle.data;
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
const market = candles[0].market;
|
||||||
|
|
||||||
|
const newCandles = candles.reduce((acc, candle) => {
|
||||||
|
if (!candle.timestamp) return acc;
|
||||||
|
if (
|
||||||
|
candleStateData[market].candles.find(
|
||||||
|
(stateCandle) => stateCandle.timestamp === candle.timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return acc;
|
||||||
|
|
||||||
|
return [
|
||||||
|
...acc,
|
||||||
|
{
|
||||||
|
date: dateFormat(
|
||||||
|
timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
)
|
||||||
|
),
|
||||||
|
datetime: timestampToDatetime(
|
||||||
|
selectedTimeType,
|
||||||
|
selectedTimeCount,
|
||||||
|
candle.timestamp
|
||||||
|
),
|
||||||
|
timestamp: candle.timestamp,
|
||||||
|
open: candle.opening_price,
|
||||||
|
high: candle.high_price,
|
||||||
|
low: candle.low_price,
|
||||||
|
close: candle.trade_price,
|
||||||
|
volume: candle.candle_acc_trade_volume,
|
||||||
|
tradePrice: candle.candle_acc_trade_price,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const newData = {
|
||||||
|
...candleStateData,
|
||||||
|
[market]: {
|
||||||
|
...candleStateData[market],
|
||||||
|
candles: [...newCandles, ...candleStateData[market].candles],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
},
|
||||||
|
marketNames: (names) => {
|
||||||
|
const data = {};
|
||||||
|
names.forEach((name) => {
|
||||||
|
if (name.market.split("-")[0] !== "KRW") return;
|
||||||
|
data[name.market] = {
|
||||||
|
korean: name.korean_name,
|
||||||
|
english: name.english_name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderbookUtils = {
|
||||||
|
init: (orderbooks, _) => {
|
||||||
|
const data = {};
|
||||||
|
orderbooks.forEach((orderbook) => {
|
||||||
|
data[orderbook.market] = {
|
||||||
|
...orderbook,
|
||||||
|
code: orderbook.market,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
update: (orderbook, state) => {
|
||||||
|
const orderbookData = state.Coin.orderbook.data;
|
||||||
|
const market = orderbook.code;
|
||||||
|
return {
|
||||||
|
...orderbookData,
|
||||||
|
[market]: {
|
||||||
|
...orderbook,
|
||||||
|
market,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tradeListUtils = {
|
||||||
|
init: (tradeLists, state) => {
|
||||||
|
const tradeListData = state.Coin.tradeList.data;
|
||||||
|
const market = tradeLists[0].market;
|
||||||
|
return {
|
||||||
|
...tradeListData,
|
||||||
|
[market]: tradeLists,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update: (tradeList, state) => {
|
||||||
|
const tradeListData = state.Coin.tradeList.data;
|
||||||
|
const market = tradeList.code;
|
||||||
|
if (
|
||||||
|
tradeListData[market] &&
|
||||||
|
tradeListData[market].find(
|
||||||
|
(data) => data.sequential_id === tradeList.sequential_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tradeListData;
|
||||||
|
|
||||||
|
// 데이터가 200개까지만 유지되게 만듦
|
||||||
|
tradeListData[market] &&
|
||||||
|
tradeListData[market].length > 200 &&
|
||||||
|
tradeListData[market].pop();
|
||||||
|
|
||||||
|
return tradeListData[market]
|
||||||
|
? {
|
||||||
|
...tradeListData,
|
||||||
|
[market]: [tradeList, ...tradeListData[market]],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...tradeListData,
|
||||||
|
[market]: [tradeList],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const choHangul = (str) => {
|
||||||
|
const cho = [
|
||||||
|
"ㄱ",
|
||||||
|
"ㄲ",
|
||||||
|
"ㄴ",
|
||||||
|
"ㄷ",
|
||||||
|
"ㄸ",
|
||||||
|
"ㄹ",
|
||||||
|
"ㅁ",
|
||||||
|
"ㅂ",
|
||||||
|
"ㅃ",
|
||||||
|
"ㅅ",
|
||||||
|
"ㅆ",
|
||||||
|
"ㅇ",
|
||||||
|
"ㅈ",
|
||||||
|
"ㅉ",
|
||||||
|
"ㅊ",
|
||||||
|
"ㅋ",
|
||||||
|
"ㅌ",
|
||||||
|
"ㅍ",
|
||||||
|
"ㅎ",
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...str].reduce((acc, cur) => {
|
||||||
|
const code = cur.charCodeAt(0) - 44032;
|
||||||
|
return code > -1 && code < 11172
|
||||||
|
? acc + cho[Math.floor(code / 588)]
|
||||||
|
: acc + cur.charAt(0);
|
||||||
|
}, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
timestampToDatetime,
|
||||||
|
candleDataUtils,
|
||||||
|
orderbookUtils,
|
||||||
|
tradeListUtils,
|
||||||
|
choHangul,
|
||||||
|
};
|
||||||
121
src/Pages/Main.js
Normal file
121
src/Pages/Main.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import withSize from "../Container/withSize";
|
||||||
|
import { viewSize } from "../styles/theme";
|
||||||
|
|
||||||
|
import Header from "../Components/Global/Header";
|
||||||
|
import CoinInfoHeader from "../Components/Main/CoinInfoHeader";
|
||||||
|
import ChartDataConsole from "../Components/Main/ChartDataConsole";
|
||||||
|
import MainChart from "../Components/Main/MainChart";
|
||||||
|
import Orderbook from "../Components/Main/Orderbook";
|
||||||
|
import OrderInfo from "../Components/Main/OrderInfo";
|
||||||
|
import TradeList from "../Components/Main/TradeList";
|
||||||
|
import CoinList from "../Components/Main/CoinList";
|
||||||
|
import Footer from "../Components/Global/Footer";
|
||||||
|
|
||||||
|
const St = {
|
||||||
|
MainContentContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 1500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@media ${({ theme }) => theme.tablet} {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
ChartAndTradeContainer: styled.section`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 95%;
|
||||||
|
max-width: 950px;
|
||||||
|
|
||||||
|
@media ${(props) => (props.isRootURL ? props.theme.tablet : true)} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HiddenH2: styled.h2`
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0, 0);
|
||||||
|
clip-path: polygon(0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -9999px;
|
||||||
|
`,
|
||||||
|
MainChartContainer: styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 500;
|
||||||
|
`,
|
||||||
|
TradeInfoContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
@media ${({ theme }) => theme.mobileM} {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
TradeOrderContainer: styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 55%;
|
||||||
|
min-width: 180px;
|
||||||
|
margin-left: 10px;
|
||||||
|
@media ${({ theme }) => theme.mobileM} {
|
||||||
|
margin-left: 0;
|
||||||
|
border: 2px solid ${({ theme }) => theme.lightGray1};
|
||||||
|
/* border-top: 1px solid ${({ theme }) => theme.lightGray1};
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.lightGray1};
|
||||||
|
border-left: 1px solid ${({ theme }) => theme.lightGray1}; */
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Main = ({ match, widthSize, heightSize }) => {
|
||||||
|
const isRootURL = match.path === "/";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header isRootURL={isRootURL} />
|
||||||
|
<St.MainContentContainer>
|
||||||
|
{
|
||||||
|
// 차트 및 주문 관련 뷰는 메인 페이지이면서 tablet 사이즈보다 크거나, 메인 페이지가 아닌 경우에만 그린다
|
||||||
|
((isRootURL && widthSize > viewSize.tablet) || !isRootURL) && (
|
||||||
|
<St.ChartAndTradeContainer isRootURL={isRootURL}>
|
||||||
|
<St.HiddenH2>차트 및 주문 정보 창</St.HiddenH2>
|
||||||
|
<CoinInfoHeader />
|
||||||
|
<ChartDataConsole />
|
||||||
|
<MainChart />
|
||||||
|
<St.TradeInfoContainer>
|
||||||
|
<Orderbook />
|
||||||
|
<St.TradeOrderContainer>
|
||||||
|
<OrderInfo />
|
||||||
|
<TradeList />
|
||||||
|
</St.TradeOrderContainer>
|
||||||
|
</St.TradeInfoContainer>
|
||||||
|
</St.ChartAndTradeContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// 코인 리스트 뷰는 메인 페이지이거나, 메인 페이지가 아니면서 tablet 사이즈보다 큰 경우에만 그린다
|
||||||
|
(isRootURL || (!isRootURL && widthSize > viewSize.tablet)) && (
|
||||||
|
<CoinList
|
||||||
|
widthSize={widthSize}
|
||||||
|
heightSize={heightSize}
|
||||||
|
isRootURL={isRootURL}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</St.MainContentContainer>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSize()(React.memo(Main));
|
||||||
547
src/Reducer/coinReducer.js
Normal file
547
src/Reducer/coinReducer.js
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
import {
|
||||||
|
createRequestSaga,
|
||||||
|
createConnectSocketThunk,
|
||||||
|
createChangeOptionSaga,
|
||||||
|
requestActions,
|
||||||
|
changeOptionActions,
|
||||||
|
requestInitActions,
|
||||||
|
createConnectSocketSaga,
|
||||||
|
} from "../Lib/asyncUtil";
|
||||||
|
import { candleDataUtils, orderbookUtils, tradeListUtils } from "../Lib/utils";
|
||||||
|
import { coinApi } from "../Api/api";
|
||||||
|
import { takeEvery, put, select } from "redux-saga/effects";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
|
const START_INIT = "coin/START_INIT";
|
||||||
|
const START_CHANGE_MARKET_AND_DATA = "coin/START_CHANGE_MARKET_AND_DATA";
|
||||||
|
const CHANGE_TIME_TYPE_AND_DATA = "coin/CHANGE_TIME_TYPE_AND_DATA";
|
||||||
|
const START_ADD_MORE_CANDLE_DATA = "coin/START_ADD_MORE_CANDLE_DATA";
|
||||||
|
|
||||||
|
const GET_MARKET_NAMES = "coin/GET_MARKET_NAMES";
|
||||||
|
const GET_MARKET_NAMES_SUCCESS = "coin/GET_MARKET_NAMES_SUCCESS";
|
||||||
|
const GET_MARKET_NAMES_ERROR = "coin/GET_MARKET_NAMES_ERROR";
|
||||||
|
|
||||||
|
const GET_INIT_CANDLES = "coin/GET_INIT_CANDLES";
|
||||||
|
const GET_INIT_CANDLES_SUCCESS = "coin/GET_INIT_CANDLES_SUCCESS";
|
||||||
|
const GET_INIT_CANDLES_ERROR = "coin/GET_INIT_CANDLES_ERROR";
|
||||||
|
|
||||||
|
const GET_ONE_COIN_CANDLES = "coin/GET_ONE_COIN_CANDLES";
|
||||||
|
const GET_ONE_COIN_CANDLES_SUCCESS = "coin/GET_ONE_COIN_CANDLES_SUCCESS";
|
||||||
|
const GET_ONE_COIN_CANDLES_ERROR = "coin/GET_ONE_COIN_CANDLES_ERROR";
|
||||||
|
|
||||||
|
const GET_ADDITIONAL_COIN_CANDLES = "coin/GET_ADDITIONAL_COIN_CANDLES";
|
||||||
|
const GET_ADDITIONAL_COIN_CANDLES_SUCCESS =
|
||||||
|
"coin/GET_ADDITIONAL_COIN_CANDLES_SUCCESS";
|
||||||
|
const GET_ADDITIONAL_COIN_CANDLES_ERROR =
|
||||||
|
"coin/GET_ADDITIONAL_COIN_CANDLES_ERROR";
|
||||||
|
|
||||||
|
const CONNECT_CANDLE_SOCKET = "coin/CONNECT_CANDLE_SOCKET";
|
||||||
|
const CONNECT_CANDLE_SOCKET_SUCCESS = "coin/CONNECT_CANDLE_SOCKET_SUCCESS";
|
||||||
|
const CONNECT_CANDLE_SOCKET_ERROR = "coin/CONNECT_CANDLE_SOCKET_ERROR";
|
||||||
|
|
||||||
|
const GET_ONE_COIN_TRADELISTS = "coin/GET_ONE_COIN_TRADELISTS";
|
||||||
|
const GET_ONE_COIN_TRADELISTS_SUCCESS = "coin/GET_ONE_COIN_TRADELISTS_SUCCESS";
|
||||||
|
const GET_ONE_COIN_TRADELISTS_ERROR = "coin/GET_ONE_COIN_TRADELISTS_ERROR";
|
||||||
|
|
||||||
|
const CONNECT_TRADELIST_SOCKET = "coin/CONNECT_TRADELIST_SOCKET";
|
||||||
|
const CONNECT_TRADELIST_SOCKET_SUCCESS =
|
||||||
|
"coin/CONNECT_TRADELIST_SOCKET_SUCCESS";
|
||||||
|
const CONNECT_TRADELIST_SOCKET_ERROR = "coin/CONNECT_TRADELIST_SOCKET_ERROR";
|
||||||
|
|
||||||
|
const GET_INIT_ORDERBOOKS = "coin/GET_INIT_ORDERBOOKS";
|
||||||
|
const GET_INIT_ORDERBOOKS_SUCCESS = "coin/GET_INIT_ORDERBOOKS_SUCCESS";
|
||||||
|
const GET_INIT_ORDERBOOKS_ERROR = "coin/GET_INIT_ORDERBOOKS_ERROR";
|
||||||
|
|
||||||
|
const CONNECT_ORDERBOOK_SOCKET = "coin/CONNECT_ORDERBOOK_SOCKET";
|
||||||
|
const CONNECT_ORDERBOOK_SOCKET_SUCCESS =
|
||||||
|
"coin/CONNECT_ORDERBOOK_SOCKET_SUCCESS";
|
||||||
|
const CONNECT_ORDERBOOK_SOCKET_ERROR = "coin/CONNECT_ORDERBOOK_SOCKET_ERROR";
|
||||||
|
|
||||||
|
const CHANGE_COIN_MARKET = "coin/CHANGE_COIN_MARKET";
|
||||||
|
const CHANGE_COIN_MARKET_SUCCESS = "coin/CHANGE_COIN_MARKET_SUCCESS";
|
||||||
|
|
||||||
|
const CHANGE_TIME_TYPE = "coin/CHANGE_TIME_TYPE";
|
||||||
|
const CHANGE_TIME_TYPE_SUCCESS = "coin/CHANGE_TIME_TYPE_SUCCESS";
|
||||||
|
|
||||||
|
const CHANGE_TIME_COUNT = "coin/CHANGE_TIME_COUNT";
|
||||||
|
const CHANGE_TIME_COUNT_SUCCESS = "coin/CHANGE_TIME_COUNT_SUCCESS";
|
||||||
|
|
||||||
|
const CHANGE_ASK_BID_ORDER = "coin/CHANGE_ASK_BID_ORDER";
|
||||||
|
const CHANGE_ASK_BID_ORDER_SUCCESS = "coin/CHANGE_ASK_BID_ORDER_SUCCESS";
|
||||||
|
|
||||||
|
const CHANGE_ORDER_PRICE = "coin/CHANGE_ORDER_PRICE";
|
||||||
|
const CHANGE_ORDER_PRICE_SUCCESS = "coin/CHANGE_ORDER_PRICE_SUCCESS";
|
||||||
|
|
||||||
|
const CHANGE_ORDER_AMOUNT = "coin/CHANGE_ORDER_AMOUNT";
|
||||||
|
const CHANGE_ORDER_AMOUNT_SUCCESS = "coin/CHANGE_ORDER_AMOUNT_SUCCESS";
|
||||||
|
|
||||||
|
const CHANGE_ORDER_TOTAL_PRICE = "coin/CHANGE_ORDER_TOTAL_PRICE";
|
||||||
|
const CHANGE_ORDER_TOTAL_PRICE_SUCCESS =
|
||||||
|
"coin/CHANGE_ORDER_TOTAL_PRICE_SUCCESS";
|
||||||
|
|
||||||
|
const CHANGE_PRICE_AND_TOTAL_PRICE = "coin/CHANGE_PRICE_AND_TOTAL_PRICE";
|
||||||
|
const CHANGE_AMOUNT_AND_TOTAL_PRICE = "coin/CHANGE_AMOUNT_AND_TOTAL_PRICE";
|
||||||
|
const CHANGE_TOTAL_PRICE_AND_AMOUNT = "coin/CHANGE_TOTAL_PRICE_AND_AMOUNT";
|
||||||
|
|
||||||
|
const SEARCH_COIN = "coin/SEARCH_COIN";
|
||||||
|
const SEARCH_COIN_SUCCESS = "coin/SEARCH_COIN_SUCCESS";
|
||||||
|
|
||||||
|
// 업비트에서 제공하는 코인/마켓 이름들 가져오기 Saga
|
||||||
|
const getMarketNameSaga = createRequestSaga(
|
||||||
|
GET_MARKET_NAMES,
|
||||||
|
coinApi.getMarketCodes,
|
||||||
|
candleDataUtils.marketNames
|
||||||
|
);
|
||||||
|
|
||||||
|
// 코인/마켓 캔들들의 일봉 한 개씩 가져오기 Saga
|
||||||
|
const getInitCandleSaga = createRequestSaga(
|
||||||
|
GET_INIT_CANDLES,
|
||||||
|
coinApi.getInitCanldes,
|
||||||
|
candleDataUtils.init
|
||||||
|
);
|
||||||
|
|
||||||
|
// 특정 코인 봉 200개 가져오기 Saga
|
||||||
|
const getOneCoinCandlesSaga = createRequestSaga(
|
||||||
|
GET_ONE_COIN_CANDLES,
|
||||||
|
coinApi.getOneCoinCandles,
|
||||||
|
candleDataUtils.oneCoin
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAdditionalCoinCandlesSaga = createRequestSaga(
|
||||||
|
GET_ADDITIONAL_COIN_CANDLES,
|
||||||
|
coinApi.getAdditionalCoinCandles,
|
||||||
|
candleDataUtils.add
|
||||||
|
);
|
||||||
|
|
||||||
|
// 캔들 웹소켓 연결 Thunk
|
||||||
|
const connectCandleSocketThunk = createConnectSocketThunk(
|
||||||
|
CONNECT_CANDLE_SOCKET,
|
||||||
|
"ticker",
|
||||||
|
candleDataUtils.update
|
||||||
|
);
|
||||||
|
|
||||||
|
// const connectCandleSocketThunk = createConnectSocketThrottleThunk(
|
||||||
|
// CONNECT_CANDLE_SOCKET,
|
||||||
|
// "ticker",
|
||||||
|
// candleDataUtils.update
|
||||||
|
// );
|
||||||
|
|
||||||
|
const connectCandleSocketSaga = createConnectSocketSaga(
|
||||||
|
CONNECT_CANDLE_SOCKET,
|
||||||
|
"ticker",
|
||||||
|
candleDataUtils.updates
|
||||||
|
);
|
||||||
|
|
||||||
|
// 호가창 조기 값 가져오기
|
||||||
|
const getInitOrderbookSaga = createRequestSaga(
|
||||||
|
GET_INIT_ORDERBOOKS,
|
||||||
|
coinApi.getInitOrderbooks,
|
||||||
|
orderbookUtils.init
|
||||||
|
);
|
||||||
|
|
||||||
|
// 호가창 웹소켓 연결 Thunk
|
||||||
|
const connectOrderbookSocketThunk = createConnectSocketThunk(
|
||||||
|
CONNECT_ORDERBOOK_SOCKET,
|
||||||
|
"orderbook",
|
||||||
|
orderbookUtils.update
|
||||||
|
);
|
||||||
|
|
||||||
|
// 체결내역 200개 가져오기
|
||||||
|
const getOneCoinTradeListsSaga = createRequestSaga(
|
||||||
|
GET_ONE_COIN_TRADELISTS,
|
||||||
|
coinApi.getOneCoinTradeLists,
|
||||||
|
tradeListUtils.init
|
||||||
|
);
|
||||||
|
|
||||||
|
// 체결내역 웹소켓 연결 Thunk
|
||||||
|
const connectTradeListSocketThunk = createConnectSocketThunk(
|
||||||
|
CONNECT_TRADELIST_SOCKET,
|
||||||
|
"trade",
|
||||||
|
tradeListUtils.update
|
||||||
|
);
|
||||||
|
|
||||||
|
// 선택한 코인마켓 변경하기 Saga
|
||||||
|
const changeSelectedMarket = (marketName) => ({
|
||||||
|
type: CHANGE_COIN_MARKET,
|
||||||
|
payload: marketName,
|
||||||
|
});
|
||||||
|
const changeSelectedMarketSaga = createChangeOptionSaga(CHANGE_COIN_MARKET);
|
||||||
|
|
||||||
|
// 선택한 타임 타입(5분봉 할때 '분') 변경하기 Saga
|
||||||
|
const changeSelectedTimeTypeSaga = createChangeOptionSaga(CHANGE_TIME_TYPE);
|
||||||
|
|
||||||
|
// 선택한 타임 카운트(5분봉 할때 '5') 변경하기 Saga
|
||||||
|
const changeSelectedTimeCountSaga = createChangeOptionSaga(CHANGE_TIME_COUNT);
|
||||||
|
|
||||||
|
// 매수 매도 옵션 변경하기
|
||||||
|
const changeAskBidOrder = (askBidOption) => ({
|
||||||
|
type: CHANGE_ASK_BID_ORDER,
|
||||||
|
payload: askBidOption,
|
||||||
|
});
|
||||||
|
const changeAskBidOrderSaga = createChangeOptionSaga(CHANGE_ASK_BID_ORDER);
|
||||||
|
|
||||||
|
// 주문 가격 변경하기
|
||||||
|
const changeOrderPriceSaga = createChangeOptionSaga(CHANGE_ORDER_PRICE);
|
||||||
|
|
||||||
|
// 주문 수량 변경하기
|
||||||
|
const changeOrderAmountSaga = createChangeOptionSaga(CHANGE_ORDER_AMOUNT);
|
||||||
|
|
||||||
|
// 주문 총액 변경하기
|
||||||
|
const changeOrderTotalPriceSaga = createChangeOptionSaga(
|
||||||
|
CHANGE_ORDER_TOTAL_PRICE
|
||||||
|
);
|
||||||
|
|
||||||
|
// 코인 검색 내용 변경하기 Saga
|
||||||
|
const searchCoin = (searchName) => ({
|
||||||
|
type: SEARCH_COIN,
|
||||||
|
payload: searchName,
|
||||||
|
});
|
||||||
|
const searchCoinSaga = createChangeOptionSaga(SEARCH_COIN);
|
||||||
|
|
||||||
|
// 시작시 데이터 초기화 작업들
|
||||||
|
const startInit = () => ({ type: START_INIT });
|
||||||
|
function* startInittSaga() {
|
||||||
|
yield getMarketNameSaga(); // 코인/시장 종류 받기
|
||||||
|
|
||||||
|
const state = yield select();
|
||||||
|
const marketNames = Object.keys(state.Coin.marketNames.data);
|
||||||
|
const selectedMarket = state.Coin.selectedMarket;
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
|
||||||
|
yield getInitCandleSaga({ payload: marketNames }); // 코인 캔들 초기값 받기
|
||||||
|
yield getInitOrderbookSaga({ payload: selectedMarket }); // 호가창 초기값 받기
|
||||||
|
yield getOneCoinTradeListsSaga({ payload: selectedMarket }); // 체결내역 초기값 받기
|
||||||
|
yield getOneCoinCandlesSaga({
|
||||||
|
payload: {
|
||||||
|
coin: selectedMarket,
|
||||||
|
timeType: selectedTimeType,
|
||||||
|
timeCount: selectedTimeCount,
|
||||||
|
},
|
||||||
|
}); // 200개 코인 데이터 받기
|
||||||
|
|
||||||
|
// yield connectCandleSocketSaga({ payload: marketNames }); // 캔들 소켓 연결 사가버전
|
||||||
|
yield put(connectOrderbookSocketThunk({ payload: marketNames })); // 오더북 소켓 연결
|
||||||
|
yield put(connectTradeListSocketThunk({ payload: marketNames })); // 체결내역 소켓 연결
|
||||||
|
// yield put(connectCandleSocketThunk({ payload: marketNames })); // 캔들 소켓 연결
|
||||||
|
yield connectCandleSocketSaga({ payload: marketNames }); // 캔들 소켓 연결 사가버전
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 코인/마켓 변경 및 해당 마켓 데이터 받기
|
||||||
|
const startChangeMarketAndData = (marketName) => ({
|
||||||
|
type: START_CHANGE_MARKET_AND_DATA,
|
||||||
|
payload: marketName,
|
||||||
|
});
|
||||||
|
function* startChangeMarketAndDataSaga(action) {
|
||||||
|
const state = yield select();
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
const changingMarketName = action.payload;
|
||||||
|
const selectedCoinCandles =
|
||||||
|
state.Coin.candle.data[changingMarketName].candles;
|
||||||
|
|
||||||
|
yield put(changeSelectedMarket(changingMarketName)); // 선택된 마켓 변경
|
||||||
|
yield getInitOrderbookSaga({ payload: changingMarketName }); // 호가창 초기값 받기
|
||||||
|
yield getOneCoinTradeListsSaga({ payload: changingMarketName }); // 체결내역 초기값 받기
|
||||||
|
|
||||||
|
// 상태에 저장된 데이터가 200개 미만일때만 api콜 요청함
|
||||||
|
if (selectedCoinCandles.length < 200) {
|
||||||
|
yield getOneCoinCandlesSaga({
|
||||||
|
payload: {
|
||||||
|
coin: changingMarketName,
|
||||||
|
timeType: selectedTimeType,
|
||||||
|
timeCount: selectedTimeCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 추가 캔들 데이터 가져오기
|
||||||
|
const startAddMoreCandleData = () => ({ type: START_ADD_MORE_CANDLE_DATA });
|
||||||
|
function* startAddMoreCandleDataSaga() {
|
||||||
|
const state = yield select();
|
||||||
|
|
||||||
|
const selectedMarket = state.Coin.selectedMarket;
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
|
||||||
|
const isLoading = state.Loading[GET_ADDITIONAL_COIN_CANDLES];
|
||||||
|
|
||||||
|
if (isLoading) return;
|
||||||
|
const datetime =
|
||||||
|
moment(state.Coin.candle.data[selectedMarket].candles[0].date)
|
||||||
|
.utc()
|
||||||
|
.format("YYYY-MM-DDTHH:mm") + ":00Z";
|
||||||
|
|
||||||
|
yield getAdditionalCoinCandlesSaga({
|
||||||
|
payload: {
|
||||||
|
coin: selectedMarket,
|
||||||
|
timeType: selectedTimeType,
|
||||||
|
timeCount: selectedTimeCount,
|
||||||
|
datetime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 차트 시간 데이터 변경하고 데이터 받기
|
||||||
|
const changeTimeTypeAndData = (timeTypeAndCount) => ({
|
||||||
|
type: CHANGE_TIME_TYPE_AND_DATA,
|
||||||
|
payload: timeTypeAndCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
function* changeTimeTypeAndDataSaga(action) {
|
||||||
|
const state = yield select();
|
||||||
|
const selectedMarket = state.Coin.selectedMarket;
|
||||||
|
const selectedTimeType = state.Coin.selectedTimeType;
|
||||||
|
const selectedTimeCount = state.Coin.selectedTimeCount;
|
||||||
|
|
||||||
|
const newTimeType = action.payload.timeType;
|
||||||
|
const newTimeCount = action.payload.timeCount;
|
||||||
|
|
||||||
|
if (selectedTimeType === newTimeType && selectedTimeCount === newTimeCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
yield changeSelectedTimeTypeSaga({ payload: newTimeType });
|
||||||
|
yield changeSelectedTimeCountSaga({ payload: newTimeCount });
|
||||||
|
|
||||||
|
yield getOneCoinCandlesSaga({
|
||||||
|
payload: {
|
||||||
|
coin: selectedMarket,
|
||||||
|
timeType: newTimeType,
|
||||||
|
timeCount: newTimeCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 가격 변경 후 주문 총액 바꾸기
|
||||||
|
const changePriceAndTotalPrice = (price) => ({
|
||||||
|
type: CHANGE_PRICE_AND_TOTAL_PRICE,
|
||||||
|
payload: price,
|
||||||
|
});
|
||||||
|
function* changePriceAndTotalPriceSaga(action) {
|
||||||
|
const state = yield select();
|
||||||
|
const orderAmount = state.Coin.orderAmount;
|
||||||
|
|
||||||
|
yield changeOrderPriceSaga({ payload: action.payload });
|
||||||
|
yield changeOrderTotalPriceSaga({
|
||||||
|
payload: Math.ceil(action.payload * orderAmount),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 주문수량 변경 후 주문 총액 바꾸기
|
||||||
|
const changeAmountAndTotalPrice = (amount) => ({
|
||||||
|
type: CHANGE_AMOUNT_AND_TOTAL_PRICE,
|
||||||
|
payload: amount,
|
||||||
|
});
|
||||||
|
function* changeAmountAndTotalPriceSaga(action) {
|
||||||
|
const state = yield select();
|
||||||
|
const orderPrice = state.Coin.orderPrice;
|
||||||
|
|
||||||
|
yield changeOrderAmountSaga({ payload: action.payload });
|
||||||
|
yield changeOrderTotalPriceSaga({
|
||||||
|
payload: Math.ceil(action.payload * orderPrice),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 주문총액 변경 후 주문수량 바꾸기
|
||||||
|
const changeTotalPriceAndAmount = (totalPrice) => ({
|
||||||
|
type: CHANGE_TOTAL_PRICE_AND_AMOUNT,
|
||||||
|
payload: totalPrice,
|
||||||
|
});
|
||||||
|
function* changeTotalPriceAndAmountSaga(action) {
|
||||||
|
const state = yield select();
|
||||||
|
const orderPrice = state.Coin.orderPrice;
|
||||||
|
|
||||||
|
yield changeOrderTotalPriceSaga({ payload: action.payload });
|
||||||
|
yield changeOrderAmountSaga({
|
||||||
|
payload: orderPrice ? (action.payload / orderPrice).toFixed(8) : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function* coinSaga() {
|
||||||
|
yield takeEvery(GET_MARKET_NAMES, getMarketNameSaga);
|
||||||
|
yield takeEvery(GET_INIT_CANDLES, getInitCandleSaga);
|
||||||
|
yield takeEvery(GET_INIT_ORDERBOOKS, getInitOrderbookSaga);
|
||||||
|
yield takeEvery(GET_ONE_COIN_CANDLES, getOneCoinCandlesSaga);
|
||||||
|
yield takeEvery(GET_ONE_COIN_TRADELISTS, getOneCoinTradeListsSaga);
|
||||||
|
|
||||||
|
yield takeEvery(CHANGE_COIN_MARKET, changeSelectedMarketSaga);
|
||||||
|
yield takeEvery(CHANGE_ASK_BID_ORDER, changeAskBidOrderSaga);
|
||||||
|
yield takeEvery(CHANGE_ORDER_PRICE, changeOrderPriceSaga);
|
||||||
|
yield takeEvery(CHANGE_ORDER_AMOUNT, changeOrderAmountSaga);
|
||||||
|
yield takeEvery(CHANGE_ORDER_TOTAL_PRICE, changeOrderTotalPriceSaga);
|
||||||
|
yield takeEvery(SEARCH_COIN, searchCoinSaga);
|
||||||
|
|
||||||
|
yield takeEvery(START_INIT, startInittSaga);
|
||||||
|
yield takeEvery(START_CHANGE_MARKET_AND_DATA, startChangeMarketAndDataSaga);
|
||||||
|
yield takeEvery(START_ADD_MORE_CANDLE_DATA, startAddMoreCandleDataSaga);
|
||||||
|
yield takeEvery(CHANGE_TIME_TYPE_AND_DATA, changeTimeTypeAndDataSaga);
|
||||||
|
|
||||||
|
yield takeEvery(CHANGE_PRICE_AND_TOTAL_PRICE, changePriceAndTotalPriceSaga);
|
||||||
|
yield takeEvery(CHANGE_AMOUNT_AND_TOTAL_PRICE, changeAmountAndTotalPriceSaga);
|
||||||
|
yield takeEvery(CHANGE_TOTAL_PRICE_AND_AMOUNT, changeTotalPriceAndAmountSaga);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
selectedMarket: "KRW-BTC",
|
||||||
|
selectedTimeType: "minutes",
|
||||||
|
selectedTimeCount: 5,
|
||||||
|
selectedAskBidOrder: "bid",
|
||||||
|
orderPrice: 0,
|
||||||
|
orderAmount: 0,
|
||||||
|
orderTotalPrice: 0,
|
||||||
|
searchCoin: "",
|
||||||
|
marketNames: {
|
||||||
|
error: false,
|
||||||
|
data: {
|
||||||
|
"KRW-BTC": "비트코인",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
candle: {
|
||||||
|
error: false,
|
||||||
|
data: {
|
||||||
|
"KRW-BTC": {
|
||||||
|
candles: [
|
||||||
|
// { date: new Date(), open: 1, close: 1, high: 1, low: 1, volume: 1 },
|
||||||
|
],
|
||||||
|
tradePrice24Hour: 0,
|
||||||
|
volume24Hour: 0,
|
||||||
|
changeRate24Hour: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderbook: {
|
||||||
|
error: false,
|
||||||
|
data: {
|
||||||
|
"KRW-BTC": {
|
||||||
|
total_bid_size: 0,
|
||||||
|
total_ask_size: 0,
|
||||||
|
orderbook_units: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tradeList: {
|
||||||
|
error: false,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const coinReducer = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
// 코인 마켓 이름들
|
||||||
|
case GET_MARKET_NAMES_SUCCESS:
|
||||||
|
case GET_MARKET_NAMES_ERROR:
|
||||||
|
return requestActions(GET_MARKET_NAMES, "marketNames")(state, action);
|
||||||
|
|
||||||
|
// 초기 캔들
|
||||||
|
case GET_INIT_CANDLES_SUCCESS:
|
||||||
|
case GET_INIT_CANDLES_ERROR:
|
||||||
|
return requestInitActions(GET_INIT_CANDLES, "candle")(state, action);
|
||||||
|
|
||||||
|
// 코인 한 개 정해서 200개
|
||||||
|
case GET_ONE_COIN_CANDLES_SUCCESS:
|
||||||
|
case GET_ONE_COIN_CANDLES_ERROR:
|
||||||
|
return requestActions(GET_ONE_COIN_CANDLES, "candle")(state, action);
|
||||||
|
|
||||||
|
// 추가 코인 데이터 로드
|
||||||
|
case GET_ADDITIONAL_COIN_CANDLES_SUCCESS:
|
||||||
|
case GET_ADDITIONAL_COIN_CANDLES_ERROR:
|
||||||
|
return requestActions(GET_ADDITIONAL_COIN_CANDLES, "candle")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
// 캔들 실시간 정보
|
||||||
|
case CONNECT_CANDLE_SOCKET_SUCCESS:
|
||||||
|
case CONNECT_CANDLE_SOCKET_ERROR:
|
||||||
|
return requestActions(CONNECT_CANDLE_SOCKET, "candle")(state, action);
|
||||||
|
|
||||||
|
// 호가창 초기값
|
||||||
|
case GET_INIT_ORDERBOOKS_SUCCESS:
|
||||||
|
case GET_INIT_ORDERBOOKS_ERROR:
|
||||||
|
return requestActions(GET_INIT_ORDERBOOKS, "orderbook")(state, action);
|
||||||
|
|
||||||
|
// 호가창 실시간 정보
|
||||||
|
case CONNECT_ORDERBOOK_SOCKET_SUCCESS:
|
||||||
|
case CONNECT_ORDERBOOK_SOCKET_ERROR:
|
||||||
|
return requestActions(CONNECT_ORDERBOOK_SOCKET, "orderbook")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
// 체결내역 200개 초기값
|
||||||
|
case GET_ONE_COIN_TRADELISTS_SUCCESS:
|
||||||
|
case GET_ONE_COIN_TRADELISTS_ERROR:
|
||||||
|
return requestActions(GET_ONE_COIN_TRADELISTS, "tradeList")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
// 체결내역 실시간 정보
|
||||||
|
case CONNECT_TRADELIST_SOCKET_SUCCESS:
|
||||||
|
case CONNECT_TRADELIST_SOCKET_ERROR:
|
||||||
|
return requestActions(CONNECT_TRADELIST_SOCKET, "tradeList")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
case CHANGE_COIN_MARKET_SUCCESS:
|
||||||
|
return changeOptionActions(CHANGE_COIN_MARKET, "selectedMarket")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
case CHANGE_TIME_TYPE_SUCCESS:
|
||||||
|
return changeOptionActions(CHANGE_TIME_TYPE, "selectedTimeType")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
case CHANGE_TIME_COUNT_SUCCESS:
|
||||||
|
return changeOptionActions(CHANGE_TIME_COUNT, "selectedTimeCount")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
case CHANGE_ASK_BID_ORDER_SUCCESS:
|
||||||
|
return changeOptionActions(CHANGE_ASK_BID_ORDER, "selectedAskBidOrder")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
case CHANGE_ORDER_PRICE_SUCCESS:
|
||||||
|
return changeOptionActions(CHANGE_ORDER_PRICE, "orderPrice")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
case CHANGE_ORDER_AMOUNT_SUCCESS:
|
||||||
|
return changeOptionActions(CHANGE_ORDER_AMOUNT, "orderAmount")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
case CHANGE_ORDER_TOTAL_PRICE_SUCCESS:
|
||||||
|
return changeOptionActions(CHANGE_ORDER_TOTAL_PRICE, "orderTotalPrice")(
|
||||||
|
state,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
|
case SEARCH_COIN_SUCCESS:
|
||||||
|
return changeOptionActions(SEARCH_COIN, "searchCoin")(state, action);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
startInit,
|
||||||
|
startChangeMarketAndData,
|
||||||
|
startAddMoreCandleData,
|
||||||
|
changeTimeTypeAndData,
|
||||||
|
coinReducer,
|
||||||
|
coinSaga,
|
||||||
|
changeAskBidOrder,
|
||||||
|
changePriceAndTotalPrice,
|
||||||
|
changeAmountAndTotalPrice,
|
||||||
|
changeTotalPriceAndAmount,
|
||||||
|
searchCoin,
|
||||||
|
};
|
||||||
15
src/Reducer/index.js
Normal file
15
src/Reducer/index.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { combineReducers } from "redux";
|
||||||
|
import { coinReducer, coinSaga } from "./coinReducer";
|
||||||
|
import { loadingReducer } from "./loadingReducer";
|
||||||
|
import { all } from "redux-saga/effects";
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
Coin: coinReducer,
|
||||||
|
Loading: loadingReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
function* rootSaga() {
|
||||||
|
yield all([coinSaga()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { rootReducer, rootSaga };
|
||||||
33
src/Reducer/loadingReducer.js
Normal file
33
src/Reducer/loadingReducer.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const START_LOADING = "loading/START_LOADING";
|
||||||
|
const FINISH_LOADING = "loading/FINISH_LOADING";
|
||||||
|
|
||||||
|
const startLoading = (payload) => ({ type: START_LOADING, payload });
|
||||||
|
const finishLoading = (payload) => ({ type: FINISH_LOADING, payload });
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
"coin/GET_ONE_COIN_CANDLES": true,
|
||||||
|
"coin/GET_INIT_ORDERBOOKS": true,
|
||||||
|
"coin/GET_ONE_COIN_TRADELISTS": true,
|
||||||
|
"coin/GET_INIT_CANDLES": true,
|
||||||
|
"coin/GET_MARKET_NAMES": true,
|
||||||
|
"coin/GET_ADDITIONAL_COIN_CANDLES": false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadingReducer = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case START_LOADING:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[action.payload]: true,
|
||||||
|
};
|
||||||
|
case FINISH_LOADING:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[action.payload]: false,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { startLoading, finishLoading, loadingReducer };
|
||||||
14
src/Router/MainRouter.js
Normal file
14
src/Router/MainRouter.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Switch, Route } from "react-router-dom";
|
||||||
|
import Main from "../Pages/Main";
|
||||||
|
|
||||||
|
const MainRouter = () => {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/" component={Main} />
|
||||||
|
<Route exact path="/trade" component={Main} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainRouter;
|
||||||
40
src/index.css
Normal file
40
src/index.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
html {
|
||||||
|
font-family: "Noto Sans CJK KR", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans CJK KR";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
src: url("/src/styles/fonts/NotoSansKR-Light.woff2") format("woff2"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Light.woff") format("woff"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Light.otf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans CJK KR";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
src: url("/src/styles/fonts/NotoSansKR-Regular.woff2") format("woff2"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Regular.woff") format("woff"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Regular.otf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans CJK KR";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url("/src/styles/fonts/NotoSansKR-Medium.woff2") format("woff2"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Medium.woff") format("woff"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Medium.otf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans CJK KR";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
src: url("/src/styles/fonts/NotoSansKR-Bold.woff2") format("woff2"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Bold.woff") format("woff"),
|
||||||
|
url("/src/styles/fonts/NotoSansKR-Bold.otf") format("truetype");
|
||||||
|
} */
|
||||||
51
src/index.js
Normal file
51
src/index.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import "core-js/stable";
|
||||||
|
import "core-js/es/set";
|
||||||
|
import "core-js/es/map";
|
||||||
|
import "regenerator-runtime/runtime";
|
||||||
|
import "raf/polyfill";
|
||||||
|
import "react-app-polyfill/ie9";
|
||||||
|
import "react-app-polyfill/stable";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import App from "./App";
|
||||||
|
import * as serviceWorker from "./serviceWorker";
|
||||||
|
|
||||||
|
import createSagaMiddleware from "redux-saga";
|
||||||
|
import ReduxThunk from "redux-thunk";
|
||||||
|
import { createStore, applyMiddleware } from "redux";
|
||||||
|
import { rootReducer, rootSaga } from "./Reducer";
|
||||||
|
import { composeWithDevTools } from "redux-devtools-extension";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ThemeProvider } from "styled-components";
|
||||||
|
import theme from "./styles/theme";
|
||||||
|
import GlobalStyle from "./styles/GlobalStyle";
|
||||||
|
|
||||||
|
// import "./index.css";
|
||||||
|
|
||||||
|
const sagaMiddleware = createSagaMiddleware();
|
||||||
|
const store = createStore(
|
||||||
|
rootReducer,
|
||||||
|
composeWithDevTools(applyMiddleware(ReduxThunk, sagaMiddleware))
|
||||||
|
);
|
||||||
|
|
||||||
|
sagaMiddleware.run(rootSaga);
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<GlobalStyle />
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
</ThemeProvider>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.unregister();
|
||||||
141
src/serviceWorker.js
Normal file
141
src/serviceWorker.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { 'Service-Worker': 'script' },
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/setupTests.js
Normal file
5
src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
18
src/styles/GlobalStyle.js
Normal file
18
src/styles/GlobalStyle.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createGlobalStyle } from "styled-components";
|
||||||
|
import normalize from "styled-normalize";
|
||||||
|
import reset from "styled-reset";
|
||||||
|
|
||||||
|
const GlobalStyle = createGlobalStyle`
|
||||||
|
${normalize}
|
||||||
|
${reset}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: rgb(231, 234, 239);
|
||||||
|
/* height: 100%; */
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default GlobalStyle;
|
||||||
BIN
src/styles/fonts/NotoSansKR-Black.otf
Normal file
BIN
src/styles/fonts/NotoSansKR-Black.otf
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Black.woff
Normal file
BIN
src/styles/fonts/NotoSansKR-Black.woff
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Black.woff2
Normal file
BIN
src/styles/fonts/NotoSansKR-Black.woff2
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Bold.otf
Normal file
BIN
src/styles/fonts/NotoSansKR-Bold.otf
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Bold.woff
Normal file
BIN
src/styles/fonts/NotoSansKR-Bold.woff
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Bold.woff2
Normal file
BIN
src/styles/fonts/NotoSansKR-Bold.woff2
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-DemiLight.otf
Normal file
BIN
src/styles/fonts/NotoSansKR-DemiLight.otf
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-DemiLight.woff
Normal file
BIN
src/styles/fonts/NotoSansKR-DemiLight.woff
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-DemiLight.woff2
Normal file
BIN
src/styles/fonts/NotoSansKR-DemiLight.woff2
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Light.otf
Normal file
BIN
src/styles/fonts/NotoSansKR-Light.otf
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Light.woff
Normal file
BIN
src/styles/fonts/NotoSansKR-Light.woff
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Light.woff2
Normal file
BIN
src/styles/fonts/NotoSansKR-Light.woff2
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Medium.otf
Normal file
BIN
src/styles/fonts/NotoSansKR-Medium.otf
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Medium.woff
Normal file
BIN
src/styles/fonts/NotoSansKR-Medium.woff
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Medium.woff2
Normal file
BIN
src/styles/fonts/NotoSansKR-Medium.woff2
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Regular.otf
Normal file
BIN
src/styles/fonts/NotoSansKR-Regular.otf
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Regular.woff
Normal file
BIN
src/styles/fonts/NotoSansKR-Regular.woff
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Regular.woff2
Normal file
BIN
src/styles/fonts/NotoSansKR-Regular.woff2
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Thin.otf
Normal file
BIN
src/styles/fonts/NotoSansKR-Thin.otf
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Thin.woff
Normal file
BIN
src/styles/fonts/NotoSansKR-Thin.woff
Normal file
Binary file not shown.
BIN
src/styles/fonts/NotoSansKR-Thin.woff2
Normal file
BIN
src/styles/fonts/NotoSansKR-Thin.woff2
Normal file
Binary file not shown.
31
src/styles/theme.js
Normal file
31
src/styles/theme.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const viewSize = {
|
||||||
|
mobileS: 480,
|
||||||
|
mobileM: 770,
|
||||||
|
tablet: 1279,
|
||||||
|
desktop: 1280,
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
deepBlue: "#093687",
|
||||||
|
skyBlue1: "rgba(0,98,223,.03)",
|
||||||
|
skyBlue2: "rgba(0,98,223,.09)",
|
||||||
|
lightPink1: "rgba(216,14,53,.03);",
|
||||||
|
lightPink2: "rgba(216,14,53,.09);",
|
||||||
|
strongRed: "#d80e35",
|
||||||
|
strongBlue: "#115DCB",
|
||||||
|
priceUp: "rgb(210, 79, 69)",
|
||||||
|
priceDown: "rgb(18, 97, 196)",
|
||||||
|
priceUpTrans: "rgba(210, 79, 69, 0.5)",
|
||||||
|
priceDownTrans: "rgba(18, 97, 196, 0.5)",
|
||||||
|
middleGray: "#00000033",
|
||||||
|
lightGray: "rgb(244, 245, 248)",
|
||||||
|
lightGray1: "rgb(249, 250, 252)",
|
||||||
|
lightGray2: "rgb(212, 214, 220)",
|
||||||
|
mobileS: `(max-width: ${viewSize.mobileS}px)`,
|
||||||
|
mobileM: `(max-width: ${viewSize.mobileM}px)`,
|
||||||
|
tablet: `(max-width: ${viewSize.tablet}px)`,
|
||||||
|
desktop: `(min-width: ${viewSize.desktop}px)`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { viewSize };
|
||||||
|
export default theme;
|
||||||
Reference in New Issue
Block a user