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