이번에는 저번시간에 이어서 우량주 매매시점 알림 대시보드의 첫번째 줄의 두번째와 세번째 카드에 대해서 제작해 보도록 하겠습니다. 해당 카드의 모습은 아래와 같습니다.
해당 카드들은 왼쪽의 MACD와 Signal선의 Cross별 통계를 보여주는 카드와, 오른쪽의 Cross별 세부 종목 리스트를 보여주는 카드로 구성되어 있기 때문에, 한번에 다루도록 하겠습니다.
두 카드간 동작은, 왼쪽카드의 chart 내 특정 data에 대한 영역을 클릭하게되면 오른쪽 카드에 해당 대상이 자동으로 표출됩니다. 만약, 빨간색의 GC영역을 클릭하면 오른쪽에 MACD vs Signal Cross Stock List에서는 Gold Cross에 해당하는 2건의 종목이 보여지게 되고... 라임색의 Point영역을 클릭하면 오른쪽에 21개의 종목이 보여지게 됩니다.
그럼 하나하나 알아가 보겠습니다. 아무래도 디자인 구성에 대해서는 우량주 매매시점 알림 Dashboard 제작 (1)에서 봤던 코드들에 대해서 재구성이기 때문에 큰 어려움없이 구성이 가능할 것 같습니다.
1. AT-Back을 통해서 제공할 API의 DTO 설계
MACD와 Signal선이 Cross하는 정보를 제공하기 위해서는 단순히 해당 API가 Select를 통해서 나온 결과를 바로 전달하는 DTO로 설계가 되어서는 곤란합니다. 왜냐하면, 해당 정보는 Gold Cross대상 종목 List와 Dead Cross대상 종목 List... 마지막으로 Point 즉 MACD선이 하락하는 상태의 대상 종목을 개별적으로 Lsit로 반환해야 하기 때문입니다.
이렇게 반환을 하게 되면, axios.get을 통해서 개별적인 list를 개별 변수로 받을테고... doughnut chart는 각 변수의 length를 dataset으로 지정하면 자연스럽게 그릴 수 있게 됩니다.
그리고 개별 List의 각 항목은 아래의 DTO를 통해서 stock 기본정보를 담습니다. 그리고 그 DTO의 집합이 MacdInfoFrontDTO의 각 구성요소가 되겠죠... 그럼 전체적인 DTO의 모습은 아래와 같습니다.
[QSSListDTO.java]
private String stocksId;
private String bsnsYear;
private String bsnsQuarter;
private String stocksRoe;
private String stocksPbr;
private String stocksMarketcap;
private String stocksType;
private String stocksName;
[MacdInfoFrontDTO.java]
private String chkDate;
private ArrayList<QSSListDTO> qssGcDetailInfo;
private ArrayList<QSSListDTO> qssDcDetailInfo;
private ArrayList<QSSListDTO> qssPointDetailInfo;
Gold Cross대상 종목을 반환하는 변수의 경우 QSSListDTO의 ArrayList로 되어 있음을 알 수 있습니다.
2. AT-Back의 /qss/macdInfo API 제작
controller부분은 아무래도 간단합니다. 하지만 service부분의 제작은 복잡할 수 있습니다. 왜냐하면, 1차로 QSS전체 리스트를 받아오고 각 리스트에 해당하는 종목별 DB에 접근해서 지정된 날짜(즉 전날)의 MACD와 Signal이 교차하는 정보를 알아내고... 그 결과에 따라서 FrontDTO에 차곡차곡 쌓아줘야 하기 때문입니다.
그럼 차례차례 알아볼까요??
[Controller.java]
@RequestMapping(value = "macdInfo", method = RequestMethod.GET)
public ResponseEntity<MacdInfoFrontDTO> macdInfo(QSSListSearch condition){
return ResponseEntity.ok(qssService.macdInfo(condition));
}
간단합니다. 다음으로는 Service 입니다. 동작로직을 우선 단계별로 확인해 보겠습니다.
- 개인별 설정된 제약조건에 따른 우량주종목 추출로직을 내부적으로 구동하여 얻어옵니다.
- vue.js에 전달할 MacdInfoFrontDTO를 우선 생성 및 초기화 해 줍니다.
- 내게 필요한 변수들을 임시로 생성해 둡니다.
- 추출된 우량주종목에 대해서 차례차례 eachTarget1 service를 통해서 개별 종목의 정보를 얻어옵니다.
- 해당 정보중에 EmaMacdCross 정보를 확인해서 Gold Cross / Dead Cross / Point 의 상태를 확인해서 임시 변수에 차곡차곡 쌓아줍니다.
- 추출된 우량주종목에 대해서 모두 확인이 완료되었다면, 임시로 생성했던 변수를 이전에 생성했던 FrontDTO에 넣어줍니다.
- 최종결과를 Controller에 전달합니다.
[Service.java]
public MacdInfoFrontDTO macdInfo(QSSListSearch condition) {
ArrayList<QSSListDTO> listQSS = listQSSPerUser(condition);
MacdInfoFrontDTO mifdto = new MacdInfoFrontDTO();
int gcCnt = 0;
int dcCnt = 0;
int naCnt = 0;
int changeCnt = 0;
ArrayList<QSSListDTO> tempQssGcDetailInfo = new ArrayList<>();
ArrayList<QSSListDTO> tempQssDcDetailInfo = new ArrayList<>();
ArrayList<QSSListDTO> tempQssPointDetailInfo = new ArrayList<>();
String chkDate = "20210311";
for(int i=0; i<listQSS.size(); i++) {
ArrayList<StockEachInfoDTO> stocksInfo = stockService.eachTarget1(listQSS.get(i).getStocksId(), chkDate);
if("GC".equals(stocksInfo.get(0).getEmaMacdCross())) {
gcCnt++;
tempQssGcDetailInfo.add(listQSS.get(i));
}else if("DC".equals(stocksInfo.get(0).getEmaMacdCross())) {
dcCnt++;
tempQssDcDetailInfo.add(listQSS.get(i));
}else {
naCnt++;
}
if("Point".equals(stocksInfo.get(0).getEmaMacdChange())) {
changeCnt++;
tempQssPointDetailInfo.add(listQSS.get(i));
}
}
mifdto.setQssGcDetailInfo(tempQssGcDetailInfo);
mifdto.setQssDcDetailInfo(tempQssDcDetailInfo);
mifdto.setQssPointDetailInfo(tempQssPointDetailInfo);
mifdto.setChkDate(chkDate);
System.out.println("GC: "+gcCnt+", DC: "+dcCnt+", NA: "+naCnt+", Change: "+changeCnt);
return mifdto;
}
기존에 알아보지 않았던 service의 method가 하나 있어서 추가로 확인해 보겠습니다. 현재 AT프로젝트의 개별 종목의 데이터에 대해서는 DB에 개별 종목의 code로 table명을 정의하고 있습니다. 예를들면... stock_009530 과 같이 말이죠. 따라서, 필연적으로 동적쿼리를 사용해야 할 때가 있습니다.
동적쿼리에 대해서는 SQL Injection에 대한 보안 이슈가 발생할 수 있기 때문에 PreparedStatement등 방지하기 위한 로직이 들어가야 합니다. 해당 방지를 위한 방법은 아래를 참조 부탁드리며...
2021.03.15 - [SpringBoot] - [Spring Boot] 26. Prevent SQL Injection
stockService.eachTarget1 메서드를 살펴보도록 하겠습니다.
[StockService.java]
public ArrayList<StockEachInfoDTO> eachTarget1(String stocksId, String targetDate) {
stocksId = "stock_" + stocksId;
String query = "SELECT * FROM " + EscapeOther.escapeTableName(stocksId) + " WHERE tDate = '" + targetDate + "'";
ArrayList<StockEachInfoDTO> backRes = eachStockInfo(query);
return backRes;
}
private ArrayList<StockEachInfoDTO> eachStockInfo(String query){
ArrayList<StockEachInfoDTO> backRes = new ArrayList<>();
Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
con = dataSource.getConnection();
stmt = con.prepareStatement(query);
rs = stmt.executeQuery();
while(rs.next()) {
StockEachInfoDTO temp = new StockEachInfoDTO();
temp.settDate(rs.getString("tDate"));
temp.setStart(rs.getDouble("start"));
temp.setHigh(rs.getDouble("high"));
temp.setLow(rs.getDouble("low"));
temp.setFinish(rs.getDouble("finish"));
temp.setMount(rs.getDouble("mount"));
temp.setMa5(rs.getDouble("ma5"));
temp.setMa10(rs.getDouble("ma10"));
temp.setMa20(rs.getDouble("ma20"));
temp.setMa60(rs.getDouble("ma60"));
temp.setMa120(rs.getDouble("ma120"));
temp.setMa240(rs.getDouble("ma240"));
temp.setEma12(rs.getDouble("ema12"));
temp.setEma26(rs.getDouble("ema26"));
temp.setEmaMACD(rs.getDouble("emaMACD"));
temp.setEmaSignal(rs.getDouble("emaSignal"));
temp.setEmaMacdCross(rs.getString("emaMacdCross"));
temp.setEmaMacdChange(rs.getString("emaMacdChange"));
backRes.add(temp);
}
rs.close();
stmt.close();
con.close();
}catch (Exception e) {
System.err.println(e);
}
return backRes;
}
아마 위에 링크한 이전글을 보시면 동작로직을 이해하실 수 있을 것 입니다.
3. AT-Font의 axios.get 부분 제작
이전 글에서 작성했던 바와 같이 axios.get을 통해서 response된 data에 대해서 바로 dataCollection에 저장하고 이어서 바로 options를 세팅하게 됩니다. 주의할 점은 크게 없고... 몇가지 특징만 캐치해서 구성하면 됩니다.
- doughnut chat의 개별 data chart부분을 click할 경우 해당 데이터로 변경되어 오른쪽 list영역에 뿌려준다.
- 최초에는 선택된 부분이 없기 때문에, default로 Gold Cross 종목 list를 뿌려준다.
간단하네요... 그럼 우선 axios.get부분 전체 코드를 보면서 이야기 해보겠습니다.
[Vue.js - getQSSMacdInfo methods]
getQSSMacdInfo() {
const url = '/qss/macdInfo';
axios.get(url, {
params: {
bsnsYear: this.searchCondition.bsnsYear,
bsnsQuarter: this.searchCondition.bsnsQuarter,
userId: this.searchCondition.userId,
},
}).then((res) => {
console.log(res.data);
this.macdInfo = res.data;
this.macdCrossItem = res.data.qssGcDetailInfo;
this.firstThirdText = 'Gold Cross ' + this.macdCrossItem.length + '건';
this.loading = false;
this.doughnutCollection2 = {
labels: ['GC', 'DC', 'Point'],
datasets: [
{
borderWidth: 5,
backgroundColor: ['#EF5350', '#42A5F5', '#C0CA33'],
hoverBackgroundColor: ['#EF5350', '#42A5F5', '#C0CA33'],
hoverBorderColor: ['#EF5350', '#42A5F5', '#C0CA33'],
// eslint-disable-next-line
data: [this.macdInfo.qssGcDetailInfo.length, this.macdInfo.qssDcDetailInfo.length, this.macdInfo.qssPointDetailInfo.length],
datalabels: { anchor: 'end' },
},
],
};
this.doughnutOptions2 = {
responsive: true,
maintainAspectRatio: false,
cutoutPercentage: 60,
legend: { display: true, position: 'bottom' },
animation: { duration: 1000 },
layout: { padding: 7 },
// click event
onClick: this.handleClickEvent,
plugins: {
datalabels: {
backgroundColor(context) {
return context.dataset.backgroundColor;
},
borderColor: 'white',
borderRadius: 25,
borderWidth: 2,
color: 'white',
display: true,
font: {
weight: 'bold',
},
padding: 6,
formatter: Math.round,
},
doughnutlabel: {
labels: [
{
text: this.macdInfo.qssGcDetailInfo.length + this.macdInfo.qssDcDetailInfo.length + this.macdInfo.qssPointDetailInfo.length,
font: {
size: '30',
weight: 'bold',
},
}, {
text: 'total',
},
],
},
},
};
}).catch((error) => {
console.log(error);
}).then(() => {
console.log('getQSSMacdInfo End!!');
});
},
이 코드에서 중요한 부분은 딱 2가지라고 말할 수 있습니다.
- 실제로 API를 통해서 /qss/macdInfo 의 response가 어떻게 생겼는지
- doughnut chart에서 어떻게 반응형으로 onClink을 구현하는지
그럼 하나하나 살펴보겠습니다.
[/qss/macdInfo response]
{chkDate: "20210311", qssGcDetailInfo: Array(2), qssDcDetailInfo: Array(0), qssPointDetailInfo: Array(21)}
chkDate: "20210311"
qssDcDetailInfo: Array(0)
qssGcDetailInfo: Array(2)
0: {…}
1: {…}
length: 2
__ob__: Observer {value: Array(2), dep: Dep, vmCount: 0}
__proto__: Array
qssPointDetailInfo: Array(21)
[세부 Info]
1:
bsnsQuarter: "1"
bsnsYear: "2020"
stocksId: "230360"
stocksMarketcap: "8764"
stocksName: "에코마케팅"
stocksPbr: "8.19"
stocksRoe: "32.7"
stocksType: "10"
설계한 대로, 결과를 도출한 입력 일자와 Gold Cross, Dead Cross, Point에 대한 종목들이 Array형태로 response되었습니다. 간단하게 보면, Gold Cross가 2건 Point가 21건이 되었네요.
그리고 각 Array내부의 개별 Info는 아래처럼 보여지게 됩니다.
[반응형 Doughnut Chart]
다음과 같이 Gold Cross를 선택한 Stock List와 Point를 선택한 Stock List는 onClick( )할 경우 다르게 보여지게 됩니다. 실제로 Chart.js에서 Event를 적용할 수 있는데 세부적인 구현방법은 아래 글을 참조 해주세요~
2021.04.01 - [Vue.js] - [Vue.js] 17. use chart.js event and label plugin listeners
[onClick method 작성 : handleClickEvent]
handleClickEvent(point, event) {
this.loading = true;
if (event[0]._index === 0) {
this.macdCrossItem = this.macdInfo.qssGcDetailInfo;
this.firstThirdText = 'Gold Cross ' + this.macdCrossItem.length + '건';
}
else if (event[0]._index === 1) {
this.macdCrossItem = this.macdInfo.qssDcDetailInfo;
this.firstThirdText = 'Dead Cross ' + this.macdCrossItem.length + '건';
}
else if (event[0]._index === 2) {
this.macdCrossItem = this.macdInfo.qssPointDetailInfo;
this.firstThirdText = 'MACD선 하락 중 ' + this.macdCrossItem.length + '건';
}
this.loading = false;
},
조건별로 공통 data( )변수에 값을 넣어주면 알아서 반응형으로 동작하게 됩니다. 그럼 대쉬보드의 첫줄에 대한 구현은 어느정도 마무리가 되었습니다.
혹시 구현을 하시는 분이 있는데, 잘 안된다 싶으면 댓글이나 쪽지남겨주시면 조금이나마 도움이 되어 드리겠습니다.
- Ayotera Lab -
댓글