-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[2주차 기본/심화 과제] 가계부 💸 #5
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
너무 잘하셔서 또 배워만 갑니다.. 저는 카테고리 추가만 로컬스토리지 활용했는데 리스트 목록까지 모두 로컬스토리지 이용해서 구현하셨네요😮 최고.. 3주차 과제도 화이팅해요 우리 ㅎㅎ
week2/assign2/script.js
Outdated
function renderList(list) { | ||
const listWrapper = document.getElementById("list-wrapper"); //리스트가 들어갈 부모 노드 | ||
const itemTemplate = document.getElementById("item-template"); //하나의 가계부 내역 아이템 템플릿 | ||
|
||
list.forEach((item) => { | ||
let itemContent = itemTemplate.cloneNode(true); //템플릿 복사 | ||
let itemNewHtml = itemContent.innerHTML; //템플릿 안의 html 복사 | ||
|
||
itemNewHtml = itemNewHtml | ||
.replace("{item-category}", item.category) | ||
.replace("{item-name}", item.name) | ||
.replace("{item-cost}", item.cost.toLocaleString()) | ||
.replace("{item-type}", item.cost < 0 ? "spending" : "income"); | ||
|
||
itemContent.innerHTML = itemNewHtml; //새롭게 바뀐 html을 템플릿에 적용 | ||
listWrapper.appendChild(itemContent.content); //부모노드 안에 넣기 | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
템플릿 태그 이용해서 훨씬 간단하게 할 수 있네요ㅠㅠ 배워갑니다!
week2/assign2/index.html
Outdated
</div> | ||
<div> | ||
<label>금액</label> | ||
<input type="text" /> | ||
<input type="text" placeholder="숫자로 입력해 주세요" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
placeholder 준 거 좋은 것 같아요 ㅎㅎ 저도 추가하겠습니다!
list = list.filter((item) => item.customId != targetId); //리스트에서 삭제한 아이템을 지움 | ||
|
||
localStorage.setItem("list_data", JSON.stringify(list)); //localStorage에 반영 | ||
console.log("삭제한거 로컬에 반영함"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
콘솔 창에 찍은 코드가 좀 남아있는데 확인차 남겨두신 걸까요?? 전 실수로 빼먹고 못지운 게 많아서 혹시나 하고 남겨봐요 ㅎㅎ
week2/assign2/category/script.js
Outdated
function handleAddIncome() { | ||
const incomeCategoryInput = document.getElementById("income-category-input"); | ||
incomeCategoryInput.addEventListener("keyup", (e) => { | ||
if (incomeCategoryInput.value !== "" && e.key === "Enter") { | ||
e.target.blur(); | ||
|
||
incomeCategoryData.push(incomeCategoryInput.value); | ||
localStorage.setItem( | ||
"income_category_data", | ||
JSON.stringify(incomeCategoryData) | ||
); | ||
renderCategoryList(incomeCategoryData); | ||
incomeCategoryInput.value = ""; | ||
} | ||
}); | ||
} | ||
|
||
function handleAddSpending() {} | ||
function handleAddSpending() { | ||
const spendingCategoryInput = document.getElementById( | ||
"spending-category-input" | ||
); | ||
spendingCategoryInput.addEventListener("keyup", (e) => { | ||
if (spendingCategoryInput.value !== "" && e.key === "Enter") { | ||
e.target.blur(); | ||
|
||
spendingCategoryData.push(spendingCategoryInput.value); | ||
localStorage.setItem( | ||
"spending_category_data", | ||
JSON.stringify(spendingCategoryData) | ||
); | ||
renderCategoryList(spendingCategoryData); | ||
spendingCategoryInput.value = ""; | ||
} | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
카테고리 추가를 수입과 지출을 함수로 나눠서 하셨네요! 전 겹치는 코드가 많은 것 같아서 하나로 묶어서 했는데 나눠서 하는 것도 좋은 것 같습니당 함수 나누는 거 너무 어려워요ㅠㅠㅠ
week2/assign2/style.css
Outdated
input[type="radio"] + label { | ||
padding: 0.8rem 3rem; | ||
border-radius: 0.5rem; | ||
|
||
background-color: var(--gray-color); | ||
color: var(--white-color); | ||
|
||
font-size: 1.6rem; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hover시에 커서가 포인터로 바뀌면 좋을 것 같아요! 내역 추가 모달 창 안에 있는 저장하기 버튼이랑 닫기 버튼까지요!
|
||
function handleFiltering() { | ||
if (incomeBtn.checked && !spendingBtn.checked) { | ||
filteredList = list.filter((item) => item.cost >= 0); | ||
renderList(filteredList); | ||
} else if (!incomeBtn.checked && spendingBtn.checked) { | ||
filteredList = list.filter((item) => item.cost < 0); | ||
renderList(filteredList); | ||
} else if (!incomeBtn.checked && !spendingBtn.checked) { | ||
renderList([]); | ||
} else { | ||
renderList(list); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전 체크박스가 체크된 개수로 먼저 나눠서 if문 작성했는데 각각의 체크박스의 체크 여부로 구현한 은서님 코드가 훨씬 가독성이 좋은 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
코드 잘 봤습니다!! 깔끔하게 잘 하신 거 같아요~~
많이 배우고 갑니다!
고생하셨습니다~~ :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런 식으로 데이터 분리하는 거 너무 좋은 거 같습니다!!
<h4 class="text-{item-type}">{item-cost}원</h4> | ||
<button class="delete-btn" type="button">X</button> | ||
</li> | ||
</template> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
우왕 이런 게 있었군요! 배워 갑니다 :)
function handleFilterBtnClick(list) { | ||
const incomeBtn = document.getElementById("checkbox-income"); | ||
const spendingBtn = document.getElementById("checkbox-spending"); | ||
|
||
let filteredList = list; | ||
|
||
function handleFiltering() { | ||
if (incomeBtn.checked && !spendingBtn.checked) { | ||
filteredList = list.filter((item) => item.cost >= 0); | ||
renderList(filteredList); | ||
} else if (!incomeBtn.checked && spendingBtn.checked) { | ||
filteredList = list.filter((item) => item.cost < 0); | ||
renderList(filteredList); | ||
} else if (!incomeBtn.checked && !spendingBtn.checked) { | ||
renderList([]); | ||
} else { | ||
renderList(list); | ||
} | ||
} | ||
|
||
incomeBtn.addEventListener("click", handleFiltering); | ||
spendingBtn.addEventListener("click", handleFiltering); | ||
|
||
renderList(filteredList); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수입이나 지출을 누른 후, 내역을 삭제하려고 하면 삭제가 되지 않는 오류가 있습니다!! 왜인지 정확히는 모르겠지만 아무래도 리스트가 변경되고 그 변경된 리스트에 대해서 삭제 모달창이 열리는 로직이 구현이 제대로 구현이 되지 않았지 않을까 라는 생각이 듭니다!
추가로 x 버튼에 cursor: pointer;가 적용되어 있지 않아요
/* Reapply the pointer cursor for anchor tags */ | ||
a, | ||
button { | ||
cursor: revert; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실제로 사용하다보면 cursor: pointer가 빠진 부분이 꽤 보이더라고요? reset.css에서 a, button에 cursor: pointer로 수정하는 것도 하나의 방법이 될 거 같아요!
let listData = []; //localStorage에 저장된 가계무 목록을 가져와 저장하는 배열 | ||
let incomeCategoryData = []; //localStorage에 저장된 수입 카테고리 목록을 가져와 저장하는 배열 | ||
let spendingCategoryData = []; //localStorage에 저장된 지출 카테고리 목록을 가져와 저장하는 배열 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
array를 let을 이용해서 선언 하신 거는 아무래도 아래 쪽에 재할당을 위해서 그러신거겠죠? 항상 const로만 선언을 했었는데 let을 이용하는 것이 더 나은 경우도 있는지에 대해 공부가 하고 싶어졌습니다 :)
(const로 선언 & push로 할당) vs (let으로 선언 & 재할당)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전체적으로 로직이 변경 사항에 대해서만 다시 계산하거나 변경하는 로직이 아니라, 하나가 변경되면 다시 처음부터 모든 것을 계산하는 방식으로 구현이 된 것 같아용(ex. countBalance) 그럼 아무래도 비효율적이겠죠?! 항상 처음부터 다시 계산하기 보다는 변경된 사항을 어떻게 반영할 수 있을지 로직을 세우면 더 좋을 것 같습니다 !
➕ 코드가 넘 길어서 JS 파일 분리를 하는 것도 좋아보여요!
심화 과제까지 고생 넘 많으셨습니당 👍
function handleFiltering() { | ||
if (incomeBtn.checked && !spendingBtn.checked) { | ||
filteredList = list.filter((item) => item.cost >= 0); | ||
renderList(filteredList); | ||
} else if (!incomeBtn.checked && spendingBtn.checked) { | ||
filteredList = list.filter((item) => item.cost < 0); | ||
renderList(filteredList); | ||
} else if (!incomeBtn.checked && !spendingBtn.checked) { | ||
renderList([]); | ||
} else { | ||
renderList(list); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 하면 매번 바뀔 때마다 새롭게 모든 리스트를 다시 훑어서 filtering해야하니까, input checkbox 여부에 따라서 className을 가지고 css 적으로 관리하는 방법도 좋을 것 같아용
deleteModalCancelBtn.addEventListener("click", () => { | ||
deleteModal.style.display = "none"; | ||
targetElement = null; | ||
}); | ||
|
||
deleteModalConfirmBtn.addEventListener("click", () => { | ||
deleteModal.style.display = "none"; | ||
|
||
targetElement && handleItemDelete(list, targetElement); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleteModalCanceBtn과 deletemodalConfirmBtn은 deleteBtns를 순회하는 동안 계속해서 addEventListener를 붙여주는건가욤?
deleteBtns.forEach((btn) => { | ||
btn.addEventListener("click", (e) => { | ||
let targetElement = e.target.parentElement; | ||
|
||
deleteModal.style.display = "flex"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이벤트 위임을 사용하면 반복문을 돌려서 일일히 이벤트리스너를 붙여주지 않아도 됩니다!
localStorage.getItem("income_category_data") === null && | ||
localStorage.setItem( | ||
"income_category_data", | ||
JSON.stringify(INCOME_CATEGORY) | ||
); | ||
localStorage.getItem("spending_category_data") === null && | ||
localStorage.setItem( | ||
"spending_category_data", | ||
JSON.stringify(SPENDING_CATEGORY) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거 category/script.js 에서도 똑같이 쓰이는 것 같은데 따로 함수화해서 export해서 써도 좋겠다!
const itemTemplate = document.getElementById("item-template"); //하나의 가계부 내역 아이템 템플릿 | ||
|
||
listWrapper.replaceChildren(); //초기화 | ||
list.forEach((item) => { | ||
let itemContent = itemTemplate.cloneNode(true); //템플릿 복사 | ||
let itemNewHtml = itemContent.innerHTML; //템플릿 안의 html 복사 | ||
|
||
itemNewHtml = itemNewHtml | ||
.replace("{item-category}", item.category) | ||
.replace("{item-name}", item.name) | ||
.replace("{item-cost}", item.cost.toLocaleString()) | ||
.replace("{item-type}", item.cost < 0 ? "spending" : "income") | ||
.replace("{item-id}", item.customId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오호! template이라는 신기한 개념을 사용했네
배포링크
✨ 구현 기능 명세
기본 과제
INIT_BALANCE
,HISTORY_LIST
를 가짐HISTORY_LIST
에 있는 수입과 지출 내역을 계산하여 반영심화 과제
💎 PR Point
상수 데이터 생성
데이터를 어떤 형태로 지정하느냐도 효율적인 코드 작성에 매우 중요함을 느낍니다. 처음에는 수입/지출 둘 중 무엇인지를 알려주는 항목을 추가해야하나 했는데, 그냥 수입이면 양수이고 지출이면 음수로 cost를 지정해놓는 것이 나중에 계산할때도 편해서 이렇게 했습니다. 또 나중에
customId
라는 항목을 추가하게 되었는데, react에서 컴포넌트 맵핑시 사용하는 key값과 비슷한 역할을 한다고 보면 될 것 같습니다. 항목을 삭제하는 기능이 필요하기 때문에 각 항목을 유니크하게 구별하는 id가 필요해서 추가했습니다.localStorage 활용
제가 localStorage를 활용한 로직은 다음과 같습니다.
위와 같은 로직으로, localStorage에는
가계부 내역 리스트
,수입 카테고리 목록
,지출 카테고리 목록
으로 총 3가지의 값을 저장했습니다.새로운 내역을 추가했을 때, 내역을 삭제했을 때, 새로운 카테고리를 추가했을 때 먼저 화면상의 렌더링을 처리한 뒤, 바뀐 데이터를 가지고 localStorage를 다시 setItem하는 방식으로 처리했습니다.
동적 렌더링
하드코딩이 아닌 동적 렌더링을 위해 저는 template 태그 라는 것을 사용했습니다. 틀을 짜놓고 원하는 콘텐츠만 바꾸고 싶을 때 사용하기 제격입니다.
html 파일을 보면 다음과 같이 template을 정의해놓았습니다.
여기서 가변적인 콘텐츠인 {item-category}, {item-name}등을 중괄호 형식으로 지정해놓고, 이 부분을 javascript를 통해 바꿔치기 하는 것입니다.
템플릿 노드를 가져와서 이를 복사하고, 이 안의 innerHTML을 복사한 뒤 해당 innerHTML에서 바꿔야할 부분을 replace() method를 통해 변경합니다. 그리고 이 변경된 내용을 아까 복사한 템플릿에 다시 넣은 뒤 appendChild로 부모 노드에 달아주는 것이죠.
나의 자산, 총수입/총지출 계산
나의 자산, 총수입, 총지출을 계산하는 함수는 다음과 같습니다.
리스트를 탐색하면서 cost가 0보다 작다면 spending이라는 변수에 더하고, 0보다 크거나 같다면 income이라는 변수에 더함으로서 간단히 구현했습니다. 이 함수는 최초 렌더링과, 항목을 추가하거나 삭제했을 때 작동합니다.
항목 삭제
항목을 삭제하는 부분은 삭제 모달을 열고 닫는 함수와, 모달 안에서 확인을 눌렀을 때 실제 삭제 동작을 처리하는 함수로 이루어집니다.
모든 X 버튼들에 eventListener를 달아서 click event를 감지합니다. 이 때
어떤 항목에 대한 X 버튼이 눌린 것인지
를 파악하기 위해, event 객체를 활용하여 e.target.parentElement를 미리 저장해둡니다. 모달에서 확인을 눌렀을 때 어떤 애를 지워야 하는지를 알 수 있도록이요. 그래서 확인 버튼이 눌리면 이 e.target.parentElement를 삭제를 동작시키는 함수로 넘겨줍니다.매개변수로 받아온 targetElement를 remove() method로 삭제해줍니다. 그러면 화면(DOM)에서만 잠깐 사라졌을 뿐, 새로고침을 하면 그대로입니다. 따라서 이를 localStorage에 반영해주어야 합니다. 따라서 지워지는 target의 id를 파악한 뒤 내역 목록에서 그 id에 해당하는 아이템을 filter() method로 삭제합니다.
list = list.filter((item) => item.customId != targetId);
이렇게 변경된
list
를 가지고 다시 localStorage에 저장한 뒤, 나의 자산과 총 지출/수입을 계산하는 함수도 다시 실행해줍니다.항목 추가
이 부분... 꽤나 총체적 난국인데요... 더 논리적이고 깔끔하게 짜고 싶었지만 시간이 없어 급하게 마무리했습니다 ㅠㅠ 여기도 마찬가지로 추가 모달을 열고 닫는 함수와, 모달 안의 form을 처리하는 함수로 나누는 것...이 저의 목적이었으나, 두 역할을 명확하게 분리해내지 못했어요.
모달을 열고 닫는 함수에서 form과 관련된 처리까지 하고 있습니다. 이 부분은 수정이 필요합니다.
<option>
태그를 만들고<select>
태그의 자식으로 넣어줍니다.addFormRadioIncome.checked ? Number(costInput.value.replaceAll(",", "")) : -1 * Number(costInput.value.replaceAll(",", ""))
🥺 소요 시간, 어려웠던 점
8h
모달을 열고 닫는 함수
와모달 안에서의 동작을 처리하는 함수
를 명확히 분리함으로써 해결했었거든요. 그래서 이번에도 그런 방향으로 접근을 했는데, 시간을 많이 쓰지 못해서... 조금씩 에러가 나기도 합니다. ㅠㅠ🌈 구현 결과물
배포링크
내역 등록 및 삭제
default.mov
수입/지출 필터링
default.mov
카테고리 추가
default.mov