본문 바로가기

[FrontEnd 웹 개발]/[웹 프로젝트]

[웹 프로젝트] Stock_Plus 주식 관련 프로젝트 (프로젝트 일지, 추천)

728x90

🚩시작하기 전..

안녕하세요! 대학생 개발자 주이어입니다. 이번에는 저번 웹 프로젝트 글에서 말했듯이 여러가지 프로젝트 아이디어가 떠올라 만들게 된 프로젝트 입니다. 저번 프로젝트를 진행하면서 깨달은 점을 활용하여 이번 프로젝트를 진행하다 보니 확실히 코드도 더 깔끔하고 조금 더 쉽게 제작했던 것 같습니다.

 

이번에 주식 관련 프로젝트를 진행하게 된 이유는 제가 요즘 주식 투자를 시작하면서 관심이 많이 생겼기 때문입니다. 만들게 된 자세한 목적은 밑에 프로젝트 정보에서 소개드리겠습니다. 

 

-이전 프로젝트 보러가기-

[웹 프로젝트] 자기소개 및 포트폴리오 사이트 만들기 (프로젝트 추천)

 

[웹 프로젝트] 자기소개 및 포트폴리오 사이트 만들기 (프로젝트 추천)

🚩시작하기 전.. 안녕하세요! 주이어입니다! 대학생이 되고 나서 이리저리 적응하다 보니 정말 오랜만에 글을 올리는 것 같습니다. 대학생이 되고 나서 왜인지 모르겠지만 코딩을 더 열심히 하

juyear-coding.tistory.com


ℹ️프로젝트 정보

개발 환경 및 도구

  • 운영 체제 : WINDOW
  • 개발 환경 : Visual Studio Code
  • 사용 언어 : Html, Css, JavaScript
  • API : Alpha Vantage API
  • 기타 : GIT

목적

  • 증권 사이트에서 가지고 있는 주식 각각의 배당률만 보여줌 → 가지고 있는 모든 주식의 평균 배당률을 알고싶음.
  • 배당주라고 평가받는 주식은 주로 배당률이 높거나, 배당률이 다른 배당주보다 높지 않지만 시세 차이로 이익을 얻을 수 있는 주식을 의미하는데 정확히 어떤 주식이 더 이득인지 쉽게 알 수 없어 조금이나마 참고자료를 만들기 위해서 만들게 됨.

개발 기간

  • 2024.11.01 ~ 2024.11.06

💻프로젝트 설명

Html CODE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="style.css">
        <title>STOCK PLUS</title>
    </head>
    <body>
        <header>
            <div id="headerTitle">
                <p id="title">STOCK <span id="colorText">PLUS</span></p>
                <p id="subTitle">가지고 있는 주식의 평균 배당률과 시세차이로 예상 수익까지 확인하세요.</p>
            </div>
        </header>
        <section>
            <button id="mainButton">확인하기</button>
            <div id="amountContainer" class="hidden">
                <p class="bodyText">총 금액</p>
                <input class="inputStyle amountInput" id="amountInput" type="text" placeholder="가지고 있는 주식의 총 금액을 입력하세요.">
            </div>
            <div id="numberContainer" class="hidden">
                <p class="bodyText">주식 갯수</p>
                <input class="inputStyle" id="numberInput" type="text" placeholder="가지고 있는 주식 종류의 갯수를 입력하세요.">
            </div>
        </section>
        <div id="alignCenter" class="hidden">
            <p class="bodyText">주식 정보</p>
        </div>
        <div id="stockInfo">
            <div id="eachNumberInput" class="hidden"></div>
            <div id="eachStockInput" class="hidden"></div>
        </div>
        <button id="completeButton" class="hidden">입력 완료</button>
        <div id="result" class="hidden">
            <p class="bodyText">결과</p>
            <hr id="line">
            <p class="resultText">총 금액 대비 배당률</p>
            <p class="resultTextRight">(프리미엄 API로 인해 제작 지연)</p>
            <p class="resultText">배당 수익금</p>
            <p class="resultTextRight">(프리미엄 API로 인해 제작 지연)</p>
            <p class="resultText">예상 1년 후 시세 증가율</p>
            <p class="resultTextRight" id="result_1"></p>
            <p class="resultText">예상 1년 후 시세 수익금</p>
            <p class="resultTextRight" id="result_2"></p>
            <p class="resultText">총 수익률(배당금 + 시세차이)</p>
            <p class="resultTextRight">-8%</p>
            <p class="resultText">총 수익금(배당금 + 시세차이)</p>
            <p class="resultTextRight">-16382원</p>
        </div>
        <footer>
            <div id="sizedBox" class="hidden">
                <p class="footerText">© 2024 Stock_Plus</p>
                <p class="footerText">제작자 : Juyear</p>
                <p class="footerText">문의 : <a id="link" href="https://github.com/Juyear009">GITHUB</a></p>
            </div>
        </footer>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
        <script src="script.js"></script>
    </body>
</html>
cs

위에는 Html 코드입니다. 저번 프로젝트에서 디자인을 고려하지 않고 Html을 작성하여 Css를 작성할 때 고생했던 점을 보완하여 이번에는 디자인 하기전부터 계획적으로 요소들을 묶어 디자인할때 효율적으로 할 수 있도록 코드를 작성했습니다. 

 

Css CODE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
@font-face {
    font-family: 'TmonsoriFont';
    src: url('font/TmonMonsori.ttf');
}
 
html {
    height: 100%;
}
 
body {
    background-color: #17171C;
    color: white;
    text-align: center;
    margin: 0;
    height: 100%;
}
 
header {
    height: 20%;
}
 
section {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    height: 80%;
    max-height: calc(100vh - 20%); /* 헤더를 제외한 최대 높이 설정 */
    overflow-y: auto;
    transition: all 0.3s ease;
}
 
#headerTitle {
    text-align: center;
    margin-bottom: 100px;
}
 
#title {
    font-size: 90px;
    margin: 0;
    padding: 30vh 0 0 0;
    font-family: 'TmonsoriFont';
    transition: padding 0.5s ease;
}
 
#colorText {
    color: #3485FA;
}
 
#subTitle {
    font-size: 20px;
    margin-top: 15px;
    color: lightgray;
}
 
#mainButton {
    font-size: 35px;
    border-radius: 10px;
    font-family: 'TmonsoriFont';
    color: white;
    background-color: #3485FA;
    transform: translateY(-1vw);
    padding: 8px 30px 4px 30px;
    cursor: pointer;
}
 
.hidden {
    display: none !important;
}
 
.bodyText {
    font-size: 30px;
    margin: 0 0 1.2vw 0;
    font-weight: 900;
    text-align: start;
    padding-left: 12px;
}
 
.inputStyle {
    width: 470px;
    height: 55px;
    border-radius: 10px;
    border: none;
    font-size: 20px;
    padding: 0 15px;
}
 
.inputStyle2 {
    width: 210px;
    height: 55px;
    border-radius: 10px;
    border: none;
    font-size: 20px;
    padding: 0 15px;
}
 
#amountContainer {
    transform: translateY(-2.5vw);
    transition: transform 0.5s ease;
}
 
#numberContainer {
    transform: translateY(-3.5vw);
    transition: transform 0.5s ease;
}
 
#alignCenter {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    transform: translateY(-18vw);
}
 
#stockInfo {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 20px;
}
 
#alignCenter .bodyText {
    width: 488px;
}
 
#eachNumberInput{
    display: flex;
    gap: 20px;
    flex-direction: column;
    transform: translateY(-18vw);
}
 
#eachStockInput{
    display: flex;
    gap: 20px;
    flex-direction: column;
    align-items: center;
    transform: translateY(-18vw);
}
 
#completeButton {
    width: 250px;
    font-size: 35px;
    border-radius: 10px;
    font-family: 'TmonsoriFont';
    color: white;
    background-color: #3485FA;
    transform: translateY(-17vw);
    padding: 8px 30px 4px 30px;
    cursor: pointer;
}
 
#result {
    border: 2px solid white;
    border-radius: 10px;
    width: 750px;
    margin: 0 auto;
    background-color: #262633;
    text-align: left;
}
 
#result .bodyText {
    text-align: center;
    margin: 20px 0;
    padding: 0;
}
 
.resultText {
    display: inline-block;
    padding-left: 5%;
    width: 45%;
    margin: 20px 0;
}
 
.resultTextRight {
    float: right;
    text-align: right;
    padding-right: 5%;
    width: 45%;
}
 
#line {
    width: 90%;
    color: lightgray;
}
 
#sizedBox {
    display: flex;
    height: 15vw;
    align-items: end;
    justify-content: center;
}
 
.footerText {
    padding: 20px 100px;
    color: lightgray;
}
 
#link {
    color: lightgray;
}
cs

위에는 Css 코드 입니다. 저번 프로젝트보다 Css코드가 훨씬 줄었는데 이번 프로젝트는 미디어 쿼리를 사용하지 않기도 했고 Html코드를 잘 작성해서 Css코드도 효율적으로 작성할 수 있었기 때문이라고 생각합니다. 무엇보다 이번 프로젝트는 JavaScript를 주로 다루는 프로젝트라 그런 것 같습니다. 간단하게 코드를 설명하자면 대부분은 디자인 관련 코드이고, 부분적으로 JavaScript와 연동하기 위한 코드들이 있습니다.

 

JavaScript CODE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
const apiKey = 'API';
 
 
document.getElementById('mainButton').addEventListener("click"function() {
    const title = document.getElementById("title");
    title.style.padding = "90px 0 0 0";
 
    this.style.display = "none";
 
    const inputContainer = document.getElementById("amountContainer");
    inputContainer.classList.remove("hidden");
});
 
function formatText(value) {
    value = value.replace(/[^0-9]/g, '');
    value = value.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
 
    return value ? " ₩ " + value : "";
}
 
function createListener(){
    document.querySelectorAll(".amountInput").forEach(input => {
        input.addEventListener("input"function() {
            const formattedValue = formatText(this.value);
            this.value = formattedValue;
        });
    });
}
 
document.getElementById("amountInput").addEventListener("keydown"function() {
    if (event.key === "Enter") {
        event.preventDefault();
        numberContainer = document.getElementById("numberContainer");
        amountContainer = document.getElementById("amountContainer");
 
        amountContainer.style.transform = "translateY(-5vw)";
        numberContainer.classList.remove("hidden");
    }
});
 
 
 
document.getElementById("numberInput").addEventListener("keydown"function() {
    if(event.key === "Enter") {
        event.preventDefault();
        const count = parseInt(document.getElementById("numberInput").value);
        let count2 = document.querySelectorAll(".inputStyle2").length/2;
        const container = document.getElementById("eachNumberInput");
        const numberContainer = document.getElementById("numberContainer");
        const amountContainer = document.getElementById("amountContainer");
        const alignCenter = document.getElementById("alignCenter");
        const eachStockInput = document.getElementById("eachStockInput");
        const completeButton = document.getElementById("completeButton");
 
        container.classList.remove("hidden");
        alignCenter.classList.remove("hidden");
        eachStockInput.classList.remove("hidden");
        completeButton.classList.remove("hidden");
 
        amountContainer.style.transform = "translateY(-7.5vw)";
        numberContainer.style.transform = "translateY(-6vw)";
 
        for (count2; count2 < count; count2++) {
            const inputField = document.createElement("input");
            inputField.type = "text";
            inputField.placeholder = String(count2+1+ ".주식 이름 입력";
            inputField.classList.add("inputStyle2");
            container.appendChild(inputField);
 
            const inputField2 = document.createElement("input");
            inputField2.type = "text";
            inputField2.placeholder = String(count2+1+ ".주식 금액 입력";
            inputField2.classList.add("inputStyle2");
            inputField2.classList.add("amountInput");
            eachStockInput.appendChild(inputField2);
        }
        for (count2; count2 > count; count2--) {
            const elements = document.querySelectorAll(".inputStyle2");
            if(elements.length > 0) {
                elements[elements.length/2-1].remove();
                elements[elements.length-1].remove();
            }
        }
        createListener();
    }
})
 
document.getElementById("completeButton").addEventListener("click"function() {
    const valueCheck = parseInt(document.getElementById("amountInput").value.slice(3).replace(/,/g,""),10);
    let temp = document.querySelectorAll(".inputStyle2");
    let tempArray = Array.from(temp);
    let half = tempArray.slice(0, tempArray.length / 2);
    let half2 = tempArray.slice(tempArray.length/2);
    let check = false;
    let prices = [];
 
    tempArray.forEach(input => {
        if(input.value.trim() === "") {
            if(check == false) {
                alert("모든 값을 입력해주세요.");
                check = true;
            }
        }
    });
 
    if(check == false) {
        const result = document.getElementById("result");
        resultText = document.querySelectorAll(".resultTextRight");
        half2.forEach(priceElements => {
            price = parseInt(priceElements.value.slice(3).replace(/,/g, ""),10);
            prices.push(price);
        });
 
        const sum = prices.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
        if(sum == valueCheck){
            fetch(`https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=KRW&apikey=${apiKey}`)
            .then(response => response.json())
            .then(data => {
                exchangeRate = parseInt(data['Realtime Currency Exchange Rate']['5. Exchange Rate'].split('.')[0]);
            });
            count3 = -1;
            avgPer = 0;
            avgPrice = 0;
            const fetchPromises = half.map(e => {
                let Symbol = e.value;
                const oneYearAgo = new Date();
                oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
                let formatDate = oneYearAgo.toISOString().split('T')[0];
                count3 += 1;
                return fetch(`https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=${Symbol}&apikey=${apiKey}&outputsize=full`)
                .then(response => response.json())
                .then(data => {
                    const timeSeries = data['Time Series (Daily)'];
                    let preStockPriceUSD;
                    if(timeSeries) {
                        const tempDate = Object.keys(timeSeries)[0];
                        const latestDate = timeSeries[tempDate];
                        const stockPriceUSD = parseFloat(latestDate['4. close']);
                        if(timeSeries[formatDate]){
                            preStockPriceUSD = parseFloat(timeSeries[formatDate]['4. close']);
                        }
                        else {
                            oneYearAgo.setDate(oneYearAgo.getDate() - 1);
                            formatDate =  oneYearAgo.toISOString().split('T')[0];
                            if(timeSeries[formatDate]){
                                preStockPriceUSD = parseFloat(timeSeries[formatDate]['4. close']);
                            }
                            else{
                                oneYearAgo.setDate(oneYearAgo.getDate() - 1);
                                formatDate =  oneYearAgo.toISOString().split('T')[0];
                                preStockPriceUSD = parseFloat(timeSeries[formatDate]['4. close']);
                            }
                        }
                        const stockPriceKRW = parseInt(stockPriceUSD * exchangeRate);
                        const preStockPriceKRW = parseInt(preStockPriceUSD * exchangeRate);
                        if(stockPriceKRW >= preStockPriceKRW) {
                            R = parseFloat(((stockPriceKRW - preStockPriceKRW) / preStockPriceKRW * 100).toFixed(2));
                            inputValue1 = document.getElementById("result_1");
                            inputValue2 = document.getElementById("result_2");
                            let calculator = parseInt((parseInt(prices[count3]) / 100* (100 + parseFloat(R)))
                            avgPer += R;
                            avgPrice += calculator;
                        }
                        else if(preStockPriceKRW > stockPriceKRW) {
                            R = parseFloat(((preStockPriceKRW - stockPriceKRW) / stockPriceKRW * 100).toFixed(2));
                            inputValue1 = document.getElementById("result_1");
                            inputValue2 = document.getElementById("result_2");
                            let calculator = parseInt((parseInt(prices[count3]) / 100* (100 - parseFloat(R)))
                            avgPer -= R;
                            avgPrice += calculator;
                        }
                    }
                });
            });
 
            Promise.all(fetchPromises).then(() => {
                changeColor();
                inputResult();
            })
            let sizedBox = document.getElementById("sizedBox");
            sizedBox.classList.remove("hidden");
            result.classList.remove("hidden");
 
            document.getElementById("result").scrollIntoView({
                behavior: 'smooth'
            });
        }
        else if(sum != valueCheck) {
            alert("총 금액과 주식 각각의 금액의 합이 같아야 합니다.\n배당이 없는 주식이더라도 입력해주세요.");
        }
    }
});
 
function inputResult() {
    inputValue1.innerText = String((avgPer / (count3+1)).toFixed(2)) + "%";
    inputValue2.innerText = String(avgPrice) + "원";
    changeColor();
}
 
function changeColor() {
    //#F04452  #3182F6
    resultText.forEach(r => {
        if(parseFloat(r.textContent.slice(0,-1)) > 0) {
            r.style.color = "#F04452";
        }
        else if(parseFloat(r.textContent.slice(0,-1)) < 0) {
            r.style.color = "#3182F6";
        }
        else if(parseFloat(r.textContent.slice(0,-1)) == 0) {
            r.style.color = "lightgray";
        }
        else {
            r.style.color = "red";
        }
    });
}
 
createListener();
cs

대망의 JS 코드입니다. 이번 프로젝트는 JS에 정말 힘을 많이 들였기 때문에... 배운 점도 많고 힘들었던 점도 굉장히 많은데요. Html관련 JS코드는 쉽게 했지만.. fetch와 then을 사용하는 비동기 함수 사용과 주식 관련 API다루는게 굉장히 어렵더라구요.

 

먼저 fetch와 then을 사용해서 특정 부분은 비동기로 진행하고 특정 부분은 동기로 진행하게 만드는 것이 많이 어려웠습니다. 이론이나 내용적인 부분은 이미 알고 있던 부분이라 어렵진 않았는데 주식 API를 연동하면서 만드니 많이 헷갈리더라구요.. 먼저 API를 사용자 입력 수에 따라 여러번 호출하게 되는데 이 모든 호출을 끝난 후에 실행하고 싶은 동기적 코드와 각각의 호출마다 진행하는 비동기적 코드를 한 함수안에서 나눠서 구현하는게 정말 어려웠습니다. 

결국은 방법을 찾고 찾아 해결했습니다. 모든 코드가 그렇듯.. 막상 방법을 찾고 보면 이해도 다되고 그렇게 어려웠던게 아니었던 것처럼 느껴집니다...ㅋㅋ 하지만 이 기회를 통해서 성장하는 거라고 생각합니다. 적어도 똑같은 경우로 다음에 또 힘들어할 일은 없으니까요..! 

 

그리고 또 열심히 만든게 애니메이션 부분입니다. 항상 웹사이트를 만들게 되면 이런 애니메이션을 구현해보고 싶다는 생각을 옛날부터 했던터라 이번 프로젝트에 넣어보고 싶었습니다. 비록 간단한 애니메이션들이지만 기본적으로 애니메이션을 어떤식으로 만드는지 파악할 수 있었습니다. 또 JS를 많이 사용해보니 정말 많은 것들을 할 수 있다는 것을 깨달았습니다. 다음 프로젝트를 진행할 땐 더 많은 것들을 할 수 있을 것 같은 생각이 들더라구요..!


😁프로젝트 결과 및 느낀 점

메인 화면
사용자 입력 화면
결과 화면

위 사진은 프로젝트 결과물 입니다! 사진으로 봐서 애니메이션이 보이진 않지만.. 실제로는 조금씩 애니메이션이 들어가 있답니다! 또 정말 아쉬운 것중 하나가.. 배당금을 알려주는 API라고 해서 사용했는데.. 배당금을 가져오려면 프리미엄 API를 돈주고 사용해야 하더라구요.. 가격이라도 싸면 최대한 해보려 했는데.. 한달에 약 6만원 가량이라.. 아쉽지만 구현하지 못했습니다. 그래도 이번 프로젝트에서 얻어간 점이 많아 만족합니다! 

 

 

지금까지 읽어주셔서 감사하고 다음에 또 다른 프로젝트로 글로 찾아오겠습니다! 

728x90