Skip to content
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주차 기본 과제 ] 가계부 #7

Merged
merged 21 commits into from
Nov 10, 2023
Merged

[ 2주차 기본 과제 ] 가계부 #7

merged 21 commits into from
Nov 10, 2023

Conversation

lydiacho
Copy link
Contributor

@lydiacho lydiacho commented Oct 27, 2023

✨ 구현 기능 명세

  • 기본 과제
  1. 최초 데이터 ✔️

    1. 상수로 INIT_BALANCE, HISTORY_LIST 데이터를 가집니다. (상수명은 자유) ✔️
      • INIT_BALANCE = 0
      • HISTORY_LIST : 입출금 내역 리스트 (4개)
    2. 최초 실행시 위 상수 데이터들 이용해 렌더링합니다. (즉, html에 직접 박아놓는 하드코딩 ❌) ✔️
      → 나의 자산은 INIT_BALANCE로부터 4개의 입출금 내역 리스트를 반영하여 보여줍니다.
  2. 총수입 / 총지출 ✔️

    1. 마찬가지로 최초에 HISTORY_LIST에 있는 수입 내역과 지출 내역을 계산해서 총수입, 총지출을 보여줍니다. ✔️
  3. 수입/지출 필터링 ✔️

    1. 수입, 지출 선택에 따라 내역 리스트가 필터링됩니다. ✔️
  4. 리스트 삭제 ✔️

    1. 각 리스트의 X 버튼을 누르면 해당 리스트가 삭제됩니다. ✔️
    2. 리스트 삭제시 나의 자산에 반영됩니다. ✔️
    3. 리스트 삭제시 총 수입 / 총 지출에 반영됩니다. ✔️
  5. 리스트 추가 ✔️

    하단 footer의 + 버튼을 누르면 리스트 추가 모달이 나타납니다.

    1. 수입 지출 버튼 ✔️
      • default ⇒ 수입
      • 하나를 선택하면 다른 항목은 자동으로 선택이 풀립니다.
    2. 카테고리를 선택 ✔️
      • 수입을 선택하면 수입 관련된 항목들이, 지출을 선택하면 종류에 지출 관련된 항목들이 나옵니다.
      • 카테고리는 수입, 지출 각각 2개 이상씩 있습니다.
    3. 금액내용 입력 input ✔️
    4. 저장하기 버튼 ✔️
      • 저장하기 버튼 클릭시 입력한 내용들로 이뤄진 리스트가 추가됩니다.
      • 이에 따라 나의 자산(잔액), 총수입, 총지출도 알맞게 변경됩니다.
      • 저장 성공시 alert 메시지를 띄웁니다.
      • 저장하기를 눌러도 모달이 닫히지 않습니다.
    5. 닫기 버튼 ✔️
      • 클릭하면 모달이 사라집니다.
  • 심화과제
  1. 리스트 삭제 모달 ✔️

    1. x 버튼 클릭시 삭제 모달이 나타납니다.

      클릭시 삭제를 진행합니다.

      취소 클릭시 모달이 사라집니다.

  2. 리스트 추가 ✔️

    1. 카테고리, 금액, 내용 중 입력하지 않은 항목이 있는데 저장하기를 누른 경우, alert를 띄워 막습니다.
    2. 금액에 숫자가 아닌 문자를 입력시 alert를 띄워 막습니다.
  3. 모달 백그라운드 & 애니메이션 (삭제, 추가) ✔️

    1. 백그라운드 : 모달 외부 부분을 어둡게 처리합니다.
    2. 애니메이션 : + 클릭시 추가 모달이 아래에서 위로 올라옵니다.
  4. 카테고리 추가

    **localStorage**를 활용합니다.

    1. 상수로 최초 카테고리를 저장한 후, 렌더링시 추가 모달의 드롭다운 option들을 동적으로 렌더링합니다.
    2. 우측 하단 버튼을 누르면 <카테고리 관리> 페이지로 이동합니다.
    3. 수입 카테고리와 지출 카테고리에 현재 카테고리들이 있습니다.
    4. input 작성 후 Enter키를 누르면 카테고리가 추가됩니다.
    5. 다시 home으로 돌아가서 내역 추가 모달을 키면 option에 새로운 카테고리가 추가되어 있습니다.
    6. 새로고침을 해도 카테고리는 계속해서 유지됩니다.
  5. 금액 ✔️

    1. 모든 금액에 세개 단위로 , 로 표시됩니다. (나의 자산, 총수입/지출, 내역 리스트, 리스트 추가 input)

💎 PR Point

▶️ 상수 데이터

상수 데이터는 다음과 같이 구조화하여 객체의 배열로 구현하였습니다.
각 객체에 삭제를 위해 ui에는 보이지 않는 id 속성을 추가하였습니다.

{
    id: 0,
    category: "식비",
    place: "청년다방청년다방청년다방청년다방청년다방청년다방",
    price: -26500,
  },

▶️ 수입/지출 필터링

  • 각 필터링 버튼에 handleFilter를 실행하는 이벤트리스너를 추가합니다.
  • handleFilter : 필터링 함수
    • input 체크 여부에 따라, plus/minus_item class에 해당하는 element들을 일괄 display:flex, display: none 처리합니다.
    • 나의 총 자산 금액에는 영향을 끼치지 않되, 리스트에만 필터링이 적용될 수 있도록 데이터를 직접 접근하는 것이 아닌 display로 처리하였습니다.
function handleFilter(e) {
  const plusInput = document.querySelector("#plus_p");
  const minusInput = document.querySelector("#minus_p");

  const plusItems = document.querySelectorAll(".plus_item");
  const minusItems = document.querySelectorAll(".minus_item");
  plusItems.forEach((item) => {
    item.style.display = plusInput.checked ? "flex" : "none";
  });
  minusItems.forEach((item) => {
    item.style.display = minusInput.checked ? "flex" : "none";
  });
}

const plusFilter = document.querySelector('label[for="plus_p"]');
plusFilter.addEventListener("click", handleFilter);
const minusFilter = document.querySelector('label[for="minus_p"]');
minusFilter.addEventListener("click", handleFilter);

그러나 해당 방식으로 구현했을 때, input 체크 여부를 동적으로 반영할 수 없는 문제에 봉착하였습니다.
(수입 버튼 클릭 시, 곧바로 checked 여부가 true가 되는 것이 아니라, 클릭한 순간에는 false로 찍히고 이후부터 true로 찍혀서 필터링도 한단계씩 지연되어 적용되는 문제)
따라서 이를 해결하고자 아래와 같이 각 checked 여부를 관리하는 flag 변수를 생성하여 관리해주었습니다.

// 수입/지출 필터링 함수
// input박스의 checked 값을 받아오는 방식 -> flag변수 사용 방식으로 변경
let plusChecked = true;
let minusChecked = true;

function handleFilter() {
  const plusItems = document.querySelectorAll(".plus_item");
  const minusItems = document.querySelectorAll(".minus_item");
  plusItems.forEach((item) => {
    item.style.display = plusChecked ? "flex" : "none";
  });
  minusItems.forEach((item) => {
    item.style.display = minusChecked ? "flex" : "none";
  });
}

const plusFilter = document.querySelector('label[for="plus_p"]');
plusFilter.addEventListener("click", function () {
  plusChecked = !plusChecked;
  handleFilter();
});
const minusFilter = document.querySelector('label[for="minus_p"]');
minusFilter.addEventListener("click", function () {
  minusChecked = !minusChecked;
  handleFilter();
});

▶️ 리스트 삭제

  • 각 아이템 상자의 X (닫기 버튼)에 handleDelete를 실행하는 이벤트 리스너를 추가하였습니다.
  • handleDelete : x버튼 클릭 시 실행되는 함수
    • 삭제 확인용 모달을 띄워줍니다 (display: flex)
    • 예 버튼 클릭 시, 리스트와 연결된 배열 데이터인 historyList에서 선택한 아이템을 splice로 삭제시켜줍니다
    • 이후 모달을 비활성화하고 (display: none)
    • 반영된 데이터를 나의 자산과 리스트에도 적용시켜줍니다 (재렌더링)
    • 취소 버튼 클릭 시, 모달을 비활성화합니다 (display: none)
function handleDelete(id) {
  const deleteModal = document.querySelectorAll(".modal_bg")[0];
  deleteModal.style.display = "flex"; // 모달 띄우기

  const deleteYesBtn = document.querySelectorAll(".delete_yes")[0];
  const deleteNoBtn = document.querySelectorAll(".delete_no")[0];
  deleteYesBtn.onclick = function () {
    //id요소 하나 삭제
    historyList.forEach((el, idx) => {
      el.id === id && historyList.splice(idx, 1);
    });
    deleteModal.style.display = "none";
    render();
  };
  deleteNoBtn.onclick = function () {
    deleteModal.style.display = "none";
  };
}

▶️ 리스트 추가

우선 하단의 추가 버튼에 이벤트 리스너를 달아 handleAddSheet 함수를 연결시켜줍니다.

const footerBtn = document.querySelector("footer>button");
footerBtn.addEventListener("click", handleAddSheet);
  • handleAddSheet : 리스트 추가 바텀시트를 띄워주는 함수
    • addFilterBtns : 수입 / 수출 버튼 클릭에 따라 selected_idx 값을 0, 1로 업데이트시켜주고, handleAddFilter 함수를 실행시킵니다
    • addSaveBtn : 저장하기 버튼.
      • 클릭 시, 바텀시트에서 작성한 정보들을 가져와 historyList에 추가해줍니다.
      • alert도 띄워줍니다.
      • 아이템이 추가된 리스트로 재렌더링시켜줍니다.
function handleAddSheet() {
  let selected_idx = 0; // 수입인지 지출인지, default : 수입 
  // ... 바텀시트 만드는 코드 

  const addFilterBtns = document.querySelectorAll(".add_filter_btn>button");
  addFilterBtns.forEach((btn, idx) => {
    btn.addEventListener("click", function () {
      selected_idx = idx;
      handleAddFilter(selected_idx);
    });
  });

  const addSaveBtn = document.querySelectorAll(".add_save_btn")[0];
  const addCloseBtn = document.querySelectorAll(".add_close_btn")[0];
  addSaveBtn.addEventListener("click", function () {
    const select = document.querySelector("#category");
    const option = select.options[select.selectedIndex].value;
    let price = document.querySelector("#add_price").value;
    const data = document.querySelector("#add_data").value;
    price = selected_idx === 1 ? -price : +price; //지출일 경우, 가격 음수처리

    historyList.push({
      id: historyList[historyList.length - 1].id + 1,
      category: option,
      place: data,
      price: price,
    });
    render();
    alert("저장되었습니다");
  });

  addCloseBtn.addEventListener("click", function () {
    addSheet.style.display = "none";
  });
}
  • handleAddFilter : 바텀시트에서 '종류' 드롭박스를 렌더링 시켜주는 함수입니다.
    • 수입, 지출 선택은 한개씩만 가능하도록 구현하고, 선택에 따라 다른 class를 부여하여 css도 처리해줍니다.
    • 선택에 따라 '종류' 드롭박스도 다르게 보여줘야하기 때문에 이렇게 별도의 함수로 분리시켰습니다.
function handleAddFilter(idx) {
  const addFilterBtns = document.querySelectorAll(".add_filter_btn>button");
  const selectTag = document.querySelector("#category");
  selectTag.innerHTML = "";

  if (idx === 0) {
    // 수입
    addFilterBtns[0].className = "selected_button";
    addFilterBtns[1].className = "unselected_button";

    const tags = ["월급", "꽁돈"];
    tags.forEach((el) => {
      const optionTag = document.createElement("option");
      optionTag.value = el;
      optionTag.textContent = el;
      selectTag.appendChild(optionTag);
    });
  } else {
    addFilterBtns[1].className = "selected_button";
    addFilterBtns[0].className = "unselected_button";

    const tags = ["식비", "교통"];
    tags.forEach((el) => {
      const optionTag = document.createElement("option");
      optionTag.value = el;
      optionTag.textContent = el;
      selectTag.appendChild(optionTag);
    });
  }
}

🥺 소요 시간, 어려웠던 점

  • 4h

🌈 구현 결과물

  • 기본과제 결과
i.e.e.e.2023-10-27.i.i.8.15.36.mov
  • 심화과제 결과
    심화과제 구현 결과물은 과제 채점 후 추후 업로드하겠습니다

@lydiacho lydiacho self-assigned this Oct 27, 2023
Copy link

@se0jinYoon se0jinYoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HISTORY_LIST 데이터로 접근해서 분기별로 렌더링 처리하는 코드가 아주 인상깊었어요 !
저랑 접근은 비슷한데 다르게 구현된 부분들을 보며 많이 배워갑니다아
새롭게 알아가는 이벤트들도 있어서 찾아보며 코드 리뷰했어요 흐흐 덕분에 새로운 것들 공부 하고 갑니다 😎

과제하느라 고생했어요오 💗💗💗

Comment on lines +39 to +41
const priceSpan = document.createElement("span");
priceSpan.textContent = (price > 0 ? "+" : "") + price.toLocaleString();
priceSpan.className = price < 0 ? "minus_price" : "plus_price";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 여기서 한 번에 처리하는 거 구웃이네요 굿굿

Comment on lines +66 to +68
historyList.forEach((el) => {
el.price > 0 ? (plus += el.price) : (minus -= el.price);
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삼항 연산자를 아주 야무지게 잘 사용하네용 👍🏻

const plusItems = document.querySelectorAll(".plus_item");
const minusItems = document.querySelectorAll(".minus_item");
plusItems.forEach((item) => {
item.style.display = plusChecked ? "flex" : "none";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 저 맨날 display: block으로 바꾸곤 했는데 생각해보니 flex로 바꾸어도 아무 이상 없겠군요...
메모메모

Comment on lines +131 to +135
const deleteModal = document.querySelectorAll(".modal_bg")[0];
deleteModal.style.display = "flex"; // 모달 띄우기

const deleteYesBtn = document.querySelectorAll(".delete_yes")[0];
const deleteNoBtn = document.querySelectorAll(".delete_no")[0];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분들 querySelectorAll 로 접근한 이유가 있을까요?
저는 중복되는 요소들이 아니기 때문에 id값으로 설정 후 접근했는데 민서도 그렇고 승희도 그렇고 querySelectorAll의 0번째 인덱스로 접근하는 코드들이 있네요 ?!
궁금합니다아

addFilterBtns[0].className = "selected_button";
addFilterBtns[1].className = "unselected_button";

const tags = ["월급", "꽁돈"];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분 이렇게 지정해두는 것 보다 HISTORY_LIST의 category랑 price 값으로 ('-'가 있으면 지출) 필터링해서 동적으로 생성할 수 있지 않을까요 ? 🤔

}
}

function handleAddSheet() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 모달 내용을 이렇게 처리했군요 !

Comment on lines +230 to +231
if (option == "미정" || price === "" || data === "") {
alert("모든 정보를 입력해주세요");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크으 예외처리 굿굿 👍🏻

? -price.replaceAll(",", "")
: +price.replaceAll(",", ""); //지출일 경우, 가격 음수처리
historyList.push({
id: historyList[historyList.length - 1].id + 1,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마지막 인덱스에 접근하고 싶다면 at()메소드를 사용해볼 수 있답니당

historyList.at(-1).id +1 

이렇게!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 너무 유익한 정보....😲🥹

addPrice.addEventListener("input", function () {
addPrice.value = (+addPrice.value.replaceAll(",", "")).toLocaleString();
});
addPrice.addEventListener("keydown", function (e) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 keydown 이벤트 첨보네요 ?! 하나 또 알아갑니당

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input에 이벤트핸들러를 사용하지 않고 keydown 이벤트를 사용할 수 있다는 걸 배워가요! keydown 이벤트 핸들러가 궁금해서 찾아보니까 숫자와 관련된 특수 키(Backspace, Delete, ArrowLeft, ArrowRight, Tab)를 허용한다는 장점이 있군요..!!

addPrice.value = (+addPrice.value.replaceAll(",", "")).toLocaleString();
});
addPrice.addEventListener("keydown", function (e) {
if (e.key < "0" || e.key > "9") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 숫자가 아닌 값이 0보다 작고 9보다 큰가? 하고 찾아봤더니 아스키코드로 비교하나 보군요!

Copy link

@rachel5640 rachel5640 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

과제하면서 진짜 이렇게 복잡한 방법으로 구현하는 방법밖에 없을까 많이 고민하는데, 승희언니 코드를 보면 진짜 그 답을 찾는것 같은 기분이 들어요! 코딩을 시작한지 얼마 안된 저도 읽기 쉽고 같은 기능인데도 다르게 구현한 코드들을 보면서 배우는게 정말 많은 것 같습니다... 이번 과제도 너무 고생 많았어용💕🥰

Comment on lines +51 to +52
const li = document.createElement("li");
li.className = price < 0 ? "minus_item" : "plus_item";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 초기 데이터에 따로 변수를 하나 추가해서 minus, plus의 상태를 구분하고 해당 값들에다가 +, - 붙여서 사용하다보니까 코드가 정말 너무 복잡해졌는데... 이렇게 간단하면서도 명료한 방법이...😭 배워갑니다!!


// 모달 나타나는 효과
addSheet.style.display = "flex";
const addSheetModal = document.querySelectorAll("add_sheet")[0];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_sheet 앞에 .이 없는것 같아요!

addPrice.addEventListener("input", function () {
addPrice.value = (+addPrice.value.replaceAll(",", "")).toLocaleString();
});
addPrice.addEventListener("keydown", function (e) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input에 이벤트핸들러를 사용하지 않고 keydown 이벤트를 사용할 수 있다는 걸 배워가요! keydown 이벤트 핸들러가 궁금해서 찾아보니까 숫자와 관련된 특수 키(Backspace, Delete, ArrowLeft, ArrowRight, Tab)를 허용한다는 장점이 있군요..!!

Comment on lines +170 to +171
addFilterBtns[1].className = "selected_button";
addFilterBtns[0].className = "unselected_button";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 체크박스로 구현해서 체크박스에 입력된 값을 계속 갱신해줘야했는데 아예 button type으로 지정하니까 더 간단하게 구현할 수도 있네요!

? -price.replaceAll(",", "")
: +price.replaceAll(",", ""); //지출일 경우, 가격 음수처리
historyList.push({
id: historyList[historyList.length - 1].id + 1,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 너무 유익한 정보....😲🥹

@lydiacho lydiacho merged commit 2467fef into main Nov 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants