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 22 commits into from
Mar 15, 2024
Merged

[ 2주차 기본/심화] 가계부 💸 #7

merged 22 commits into from
Mar 15, 2024

Conversation

Yeonseo-Jo
Copy link
Contributor

✨ 구현 기능 명세

⭐️기본 과제

  • 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

1️⃣ 상수 데이터로 나의 자산, 총 수입, 총 지출, 내역 리스트 초기 값 렌더링 (기본)

    1. 상수 데이터 사용 => INIT_BALANCE, SUM_EXPENSE, SUM_INCOME, HISTORY_LIST_DATA
    1. 상수 데이터 활용해서 js로 초기 값 렌더링
// 내역 리스트 렌더링 함수
const renderHistory = () => {
  const historyContainer = $(".history__list__container");

  HISTORY_LIST_DATA.forEach((list) => {
    const { id, category, contents, type, amount } = list;

    const historyBox = document.createElement("li");
    historyBox.classList.add(`history__list__box`);
    historyBox.id = id;
    historyBox.innerHTML = `
    <span class="history-category">${category}</span>
    <span class="history-contents">${contents}</span>
    <span class="history-amount ${type}">${amount.toLocaleString()}</span>
    <button type="button" class="history-del-btn">X</button>`;

    historyContainer.appendChild(historyBox);
  });
};

// 총 자산, 수입, 지출 렌더링 함수
const renderTotalBalance = () => {
  const incomeAmounts = [...$$(".history-amount.income")].map((history) => {
    return Number(history.innerHTML.replaceAll(",", ""));
  });
  const expenseAmounts = [...$$(".history-amount.expense")].map((history) => {
    return Number(history.innerHTML.replaceAll(",", ""));
  });

  SUM_INCOME = incomeAmounts.reduce((sum, curr) => {
    return sum + curr;
  }, 0);

  SUM_EXPENSE = expenseAmounts.reduce((sum, curr) => {
    return sum + curr;
  }, 0);

  const totalAmount = $(".asset__box__total-amount");
  totalAmount.innerHTML = (
    INIT_BALANCE +
    SUM_INCOME -
    SUM_EXPENSE
  ).toLocaleString();

  const totalExpense = $(".detail-amount__num__minus");
  totalExpense.innerHTML = SUM_EXPENSE.toLocaleString();

  const totalIncome = $(".detail-amount__num__plus");
  totalIncome.innerHTML = SUM_INCOME.toLocaleString();
};

// 초기 데이터 렌더링 함수
const handleRenderInitData = () => {
  renderHistory(); //내역 리스트와
  renderTotalBalance(); // 총 수입, 지출, 자산을 데이터로부터 가져와 보여준다
};

2️⃣ 수입 지출 필터링 (기본)

  • 체크박스 선택에 따라 수입, 지출 필터링 기능 구현
  • 필터링은 display를 js로 none, flex로 바꿔가며 구현
const incomeCheckbox = $("#checkbox-income");
const expenseCheckbox = $("#checkbox-expense");

//리스트 필터 함수
const filterList = () => {
  const incomeLists = $$(".history-amount.income");
  const expenseLists = $$(".history-amount.expense");

  incomeCheckbox.checked
    ? incomeLists.forEach((list) => (list.parentNode.style.display = "flex"))
    : incomeLists.forEach((list) => (list.parentNode.style.display = "none"));

  expenseCheckbox.checked
    ? expenseLists.forEach((list) => (list.parentNode.style.display = "flex"))
    : expenseLists.forEach((list) => (list.parentNode.style.display = "none"));
};

//체크 박스 이벤트에 의한 필터링 헨들링 함수
const handleFilterCheckbox = () => {
  incomeCheckbox.addEventListener("change", filterList);
  expenseCheckbox.addEventListener("change", filterList);
};

3️⃣ 리스트 삭제 (기본)

  • js로 버튼 이벤트 감지 해서 리스트 삭제
    • 삭제는 노드를 remove() 해서 구현
  • 삭제 된 후 자산에 반영하기 위해 renderTotalBalance() 함수 재사용하여 실행
//리스트 삭제 함수
const delData = (event) => {
  //이벤트 전파 방지 조건문
  if (event.target.className === "history-del-btn") {
    event.target.parentNode.remove(); //리스트 삭제
  }

  //총 자산에도 반영
  renderTotalBalance();
};

// 리스트 삭제 버튼 클릭시 삭제 구현 핸들링 함수
const handleDelList = () => {
  const delBtns = $$(".history-del-btn");

  delBtns.forEach(() => addEventListener("click", delData));
};

4️⃣ 리스트 추가 (기본 + 심화)

  • 리스트 추가 모달 만들어서 기능 구현
    1. 수입, 지출 (type) : radio 버튼으로 하나만 선택할수 있도록 구현
    1. cateogory 선택 : select - option 태그로 구현, type이 무엇인지 가져와서 js로 동적 렌더링
    1. 금액 입력 유효성 검사 : 숫자만 입력할 수 있도록 isNaN으로 입력값 검사 + 정규식 확용해서 숫자만 보이도록 구현
    1. 저장하기, 닫기 구현
    1. 입력하지 않은 항목 있는지 조건문 (&&)으로 구현
    1. 모달 아래에서 위로 올라오게 애니메이션 : fade-in keyframe 정의해서 스타일링 (css)
//리스트 추가 모달 나타나게 하는 핸들러 함수
const handleOpenListAddModal = () => {
  const openBtn = $(".footer__add-btn");

  openBtn.addEventListener("click", () => {
    addListModal.style.display = "block";

    // 초기 값 세팅
    $(".add-amounts__input").value = "";
    $(".add-contents__input").value = "";
    $(
      ".add-category__select"
    ).innerHTML = `<option class="add-category__option">알바비</option>
    <option class="add-category__option">용돈</option>`;
  });
};

// type 버튼 이벤트 감지 핸들러 함수
const handleChangeType = () => {
  const radioInput = $(".add-list-modal__radio-input");
  radioInput.addEventListener("change", renderOptions);
};

// select의 option을 type에 따라 렌더해주는 함수
const renderOptions = (event) => {
  let targetType = "income"; //default 값

  if (event.target.id === "modal__radio__expense") {
    targetType = "expense";
  }

  const categorySelect = $(".add-category__select");

  targetType === "income"
    ? (categorySelect.innerHTML = `
  <option class="add-category__option">알바비</option>
  <option class="add-category__option">용돈</option>`)
    : (categorySelect.innerHTML = `
  <option class="add-category__option">식비</option>
  <option class="add-category__option">쇼핑</option>`);
};

//숫자만 입력하도록 구현하는 함수
const checkNumber = (event) => {
  if (isNaN(event.key)) {
    alert("숫자만 입력하세요");
  }
  event.target.value = Number(
    event.target.value.replace(/[^0-9]/g, "")
  ).toLocaleString();
};

// 금액에 숫자만 입력하도록 하는 핸들러 함수
const handleEnterAmount = () => {
  const newAmount = $(".add-amounts__input");

  newAmount.addEventListener("keyup", checkNumber);
};

// 리스트 추가 함수
const addNewList = () => {
  const newType = $('input[name="type"]:checked').value;

  const categories = $(".add-category__select");
  const newCategory = categories.options[categories.selectedIndex].innerHTML;

  const newAmount = $(".add-amounts__input").value;

  const newContents = $(".add-contents__input").value;

  const historyContainer = $(".history__list__container");
  const historyBox = document.createElement("li");
  historyBox.classList.add(`history__list__box`);
  historyBox.innerHTML = `
  <span class="history-category">${newCategory}</span>
  <span class="history-contents">${newContents}</span>
  <span class="history-amount ${newType}">${newAmount.toLocaleString()}</span>
  <button type="button" class="history-del-btn">X</button>`;

  if (newCategory && newAmount && newContents) {
    historyContainer.appendChild(historyBox);
    alert("저장 되었습니다.");
  } else {
    alert("아직 입력되지 않은 항목이 있습니다.");
  }
};

// 모달에서 저장 버튼 클릭시 리스트 추사하는 핸들러 함수
const handleAddList = () => {
  const saveBtn = $(".add-list-modal__save-btn");

  saveBtn.addEventListener("click", addNewList);
};

// 모달을 닫아주는 핸들러 함수
const handleCloseListAddModal = () => {
  const closeBtn = $(".add-list-modal__close-btn");
  closeBtn.addEventListener("click", () => {
    addListModal.style.display = "none";
  });
};

5️⃣ 금액 3개 단위로 , 표시

  • toLocaleString() 활용
  • 다만, 자산 내역에서 읽어와서 총 자산 계산하는 로직에서는 다시 숫자로 인식할수 있게 replaceAll 메서드 사용
//1. 총 자산 부분에서 적용
onst renderTotalBalance = () => {
  const incomeAmounts = [...$$(".history-amount.income")].map((history) => {
    return Number(history.innerHTML.replaceAll(",", ""));
  });
  const expenseAmounts = [...$$(".history-amount.expense")].map((history) => {
    return Number(history.innerHTML.replaceAll(",", ""));
  });

  SUM_INCOME = incomeAmounts.reduce((sum, curr) => {
    return sum + curr;
  }, 0);

  SUM_EXPENSE = expenseAmounts.reduce((sum, curr) => {
    return sum + curr;
  }, 0);

  const totalAmount = $(".asset__box__total-amount");
  totalAmount.innerHTML = (
    INIT_BALANCE +
    SUM_INCOME -
    SUM_EXPENSE
  ).toLocaleString();

  const totalExpense = $(".detail-amount__num__minus");
  totalExpense.innerHTML = SUM_EXPENSE.toLocaleString();

  const totalIncome = $(".detail-amount__num__plus");
  totalIncome.innerHTML = SUM_INCOME.toLocaleString();
};


//2. 리스트 추가 모달에서도 적용
//숫자만 입력하도록 구현하는 함수
const checkNumber = (event) => {
  if (isNaN(event.key)) {
    alert("숫자만 입력하세요");
  }
  event.target.value = Number(
    event.target.value.replace(/[^0-9]/g, "")
  ).toLocaleString();
};

🥺 소요 시간, 어려웠던 점

  • 4h ~ 5h
  • 바닐라 자스 어렵다 222
  • 다 못한 심화과제는.. 주말에 하겠습니당!

🌈 구현 결과물

배포 링크

@Yeonseo-Jo Yeonseo-Jo self-assigned this Oct 26, 2023
Copy link
Member

@nayujin-dev nayujin-dev left a comment

Choose a reason for hiding this comment

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

넘 고생많았어 :octocat: 코드가 넘 깔끔하고 !!! 선생각 후코딩 했다는게 완전 잘 느껴져!!! 나도 진짜 담부터는 선생각 후코딩 해야지 ,,

@@ -0,0 +1,30 @@
export const HISTORY_LIST_DATA = [
Copy link
Member

Choose a reason for hiding this comment

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

엇 코드 깔끔하려면 더미데이터 분리해야하는거..!! 난 아무생각없이 그냥 한 파일에 다 넣어부럿다 역시 연서는 꼼꼼 & 실력자 ,,

Copy link
Member

Choose a reason for hiding this comment

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

나도 한 파일에...ㅎㅎㅎㅎ

@@ -0,0 +1,229 @@
import { HISTORY_LIST_DATA } from "./data/HISTORY_LIST.js";
Copy link
Member

Choose a reason for hiding this comment

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

나도 이렇게할걸 ,, , ,,

@@ -0,0 +1,229 @@
import { HISTORY_LIST_DATA } from "./data/HISTORY_LIST.js";

const $ = (selector) => document.querySelector(selector);
Copy link
Member

Choose a reason for hiding this comment

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

그리고 나 이거 연서 덕에 처음알았어 !! 템플릿 스트링이자 document.getElementById() 함수의 바로가기 기능을 한다는 ,, 난 이거 모르고 그냥 하나하나 다 코딩했다 키키 배워갑니당

const historyBox = document.createElement("li");
historyBox.classList.add(`history__list__box`);
historyBox.id = id;
historyBox.innerHTML = `
Copy link
Member

Choose a reason for hiding this comment

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

와 미쳤다 리터럴처리 미쳤다 ..

Copy link
Member

Choose a reason for hiding this comment

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

이렇게 해도 되는구나....

};

// 총 자산, 수입, 지출 렌더링 함수
const renderTotalBalance = () => {
Copy link
Member

Choose a reason for hiding this comment

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

오 렌더링 새로해야할때마다 얘를 호출하는거구나!! 히히 나도 나름 비슷하게 생각한거같아서 다행스 ㅎ.ㅎ

}, 0);

const totalAmount = $(".asset__box__total-amount");
totalAmount.innerHTML = (
Copy link
Member

Choose a reason for hiding this comment

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

구조가 넘 깔끔해요 ..


incomeCheckbox.checked
? incomeLists.forEach((list) => (list.parentNode.style.display = "flex"))
: incomeLists.forEach((list) => (list.parentNode.style.display = "none"));
Copy link
Member

Choose a reason for hiding this comment

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

와 여기 미쳤다 난 아예 filter용 리스트를 하나 만들어서 checkbox 수정될때마다 해당 조건에 따른 필터링 요소들을 넣었는데 이렇게하면 굳이 여러번 작업 안해도되니까 훨~~씬 효율적일거같다!!

Copy link
Member

Choose a reason for hiding this comment

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

아예 style을 바꿔주니까 내역리스트를 건드릴 필요가 없네!!

$(
".add-category__select"
).innerHTML = `<option class="add-category__option">알바비</option>
<option class="add-category__option">용돈</option>`;
Copy link
Member

Choose a reason for hiding this comment

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

내가 짤때는 element 새로 만들고 그안에 innerText 설정하느라 세네줄 코딩했던거같은데 이거 진짜 경험치에서 나오는 센스 미쳤어

Copy link
Member

Choose a reason for hiding this comment

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

속성이 많은 애들은 innerHTML로 해주는게 더 편하규만,,,

const handleEnterAmount = () => {
const newAmount = $(".add-amounts__input");

newAmount.addEventListener("keyup", checkNumber);
Copy link
Member

Choose a reason for hiding this comment

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

오 keyup으로 이벤트 줬구나!! 나는 그냥 change로 줬는데 keyup으로 주는것도 알아봐야겠다

Copy link
Member

@SooY2 SooY2 left a comment

Choose a reason for hiding this comment

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

함수에 한 기능만 있도록 신경쓴게 보인다! 그리고 전체적으로 너무 깔끔하고 센스있는 코드야✨ 넘넘 수고했어!!!

@@ -0,0 +1,30 @@
export const HISTORY_LIST_DATA = [
Copy link
Member

Choose a reason for hiding this comment

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

나도 한 파일에...ㅎㅎㅎㅎ

const historyBox = document.createElement("li");
historyBox.classList.add(`history__list__box`);
historyBox.id = id;
historyBox.innerHTML = `
Copy link
Member

Choose a reason for hiding this comment

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

이렇게 해도 되는구나....

// 총 자산, 수입, 지출 렌더링 함수
const renderTotalBalance = () => {
const incomeAmounts = [...$$(".history-amount.income")].map((history) => {
return Number(history.innerHTML.replaceAll(",", ""));
Copy link
Member

Choose a reason for hiding this comment

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

history-amount와 income을 동시에 가진 모든 HTML 요소를 찾아서 그 요소들의 내용(innerHTML)을 숫자로 변환해서 배열을 생성한 거구나!! 이런방법도 있군...
근데 궁금한점!! ...이 스프레드 연산자가 여기서 무슨역할 하는건지 궁금해!


incomeCheckbox.checked
? incomeLists.forEach((list) => (list.parentNode.style.display = "flex"))
: incomeLists.forEach((list) => (list.parentNode.style.display = "none"));
Copy link
Member

Choose a reason for hiding this comment

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

아예 style을 바꿔주니까 내역리스트를 건드릴 필요가 없네!!

$(
".add-category__select"
).innerHTML = `<option class="add-category__option">알바비</option>
<option class="add-category__option">용돈</option>`;
Copy link
Member

Choose a reason for hiding this comment

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

속성이 많은 애들은 innerHTML로 해주는게 더 편하규만,,,

handleOpenListAddModal();
handleChangeType();
handleAddList();
handleCloseListAddModal();
Copy link
Member

Choose a reason for hiding this comment

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

이함수는 modal나타나게하는 함수랑 별개로 이렇게 전역으로 호출해줘도 event발생할 때마다 알아서 해주는건가...?
전역으로 호출해주는거랑 함수안에서 또 함수 호출해주는게 좀 헷갈려!!

@Yeonseo-Jo Yeonseo-Jo changed the title [ 2주차 기본/심화(to be continue..) ] 가계부 💸 [ 2주차 기본/심화] 가계부 💸 Mar 15, 2024
@Yeonseo-Jo Yeonseo-Jo merged commit d614dfb into main Mar 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants