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주차 기본/심화 과제] 가계부 💸 #5

Merged
merged 24 commits into from
Feb 13, 2024
Merged

[2주차 기본/심화 과제] 가계부 💸 #5

merged 24 commits into from
Feb 13, 2024

Conversation

simeunseo
Copy link
Member

@simeunseo simeunseo commented Oct 27, 2023

배포링크

✨ 구현 기능 명세

  • 기본 과제

    • 최초 데이터
      • 상수로 INIT_BALANCE, HISTORY_LIST를 가짐
      • 최초 실행시 위 상수로 렌더링함
      • 나의 자산에 반영
    • 총수입/총지출
      • HISTORY_LIST에 있는 수입과 지출 내역을 계산하여 반영
    • 수입/지출 버튼 선택에 따른 필터링
    • 리스트 삭제
      • 각 리스트의 X 버튼을 누르면 리스트 삭제
      • 리스트 삭제시 나의 자산에 반영
      • 리스트 삭제시 총 수입/지출에 반영
    • 리스트 추가
      • 하단 + 버튼 누르면 리스트 추가 모달 나옴
      • 디폴트는 수입 버튼이 클릭되어 있음
      • 수입/지출 중 하나를 선택하면 다른 항목은 자동으로 선택이 풀림
      • 수입/지출 각각에 대한 카테고리가 나옴
      • 금액과 내용 input
      • 저장하기 버튼 클릭시 입력한 내용으로 리스트 추가
      • 추가된 리스트에 따라 자산, 총수입/지출 계산
      • 저장 성공시 alert
      • 저장하기를 눌러도 모달이 닫히지 않음
      • 닫기 버튼을 누르면 모달이 사라짐
  • 심화 과제

    • 리스트 삭제 모달
      • X 버튼 클릭시 삭제 모달 나타남
      • 확인을 클릭하면 삭제됨
      • 취소를 클릭하면 모달이 사라짐
    • 리스트 추가
      • 입력하지 않은 항목이 있으면 alert를 띄워 막음
      • 금액에 문자를 입력하면 alert를 띄워 막음
    • 모달
      • 백그라운드 어둡게 처리
      • 아래에서 위로 올라오는 애니메이션
    • 카테고리 추가
      • localStorage 활용하여 카테고리 저장
      • 상수로 최초 카테고리 저장
      • 아이템 추가 모달의 드롭다운 option 동적 렌더링
      • 카테고리 관리 페이지 이동
      • 수입/지출 각각 input 입력과 Enter키에 따라 카테고리 추가
      • home으로 돌아가면 option에 새로 추가된 카테고리 반영
      • 새로고침해도 추가된 카테고리 유지
    • 금액
      • 모든 금액에 세개 단위로 컴마 표시

💎 PR Point

  • 상수 데이터 생성

      export const LIST_DATA = [
      {
          customId: 1,
          category: "식비",
          name: "크라이치즈버거 역곡점",
          cost: -10800,
      },
      {
          customId: 2,
          category: "취미",
          name: "포토그레이 부천점",
          cost: -4000,
      },
      {
          customId: 3,
          category: "월급",
          name: "근로장학",
          cost: 300000,
      },
      {
          customId: 4,
          category: "쇼핑",
          name: "풋락커 커먼그라운드점",
          cost: -99000,
      },
      ];
    
      export const INCOME_CATEGORY = ["월급", "용돈"];
    
      export const SPENDING_CATEGORY = ["식비", "쇼핑", "취미"];          

    데이터를 어떤 형태로 지정하느냐도 효율적인 코드 작성에 매우 중요함을 느낍니다. 처음에는 수입/지출 둘 중 무엇인지를 알려주는 항목을 추가해야하나 했는데, 그냥 수입이면 양수이고 지출이면 음수로 cost를 지정해놓는 것이 나중에 계산할때도 편해서 이렇게 했습니다. 또 나중에 customId라는 항목을 추가하게 되었는데, react에서 컴포넌트 맵핑시 사용하는 key값과 비슷한 역할을 한다고 보면 될 것 같습니다. 항목을 삭제하는 기능이 필요하기 때문에 각 항목을 유니크하게 구별하는 id가 필요해서 추가했습니다.

  • localStorage 활용

    제가 localStorage를 활용한 로직은 다음과 같습니다.

    1. window.onload시 localStorage에 값이 있는지 확인
    2. 값이 없다면 -> js파일에 정의해놓은 상수를 localStorage에 setItem
    3. 값이 있다면 -> 해당 값을 getItem 하여 전역변수에 저장

    위와 같은 로직으로, localStorage에는 가계부 내역 리스트, 수입 카테고리 목록, 지출 카테고리 목록으로 총 3가지의 값을 저장했습니다.

    새로운 내역을 추가했을 때, 내역을 삭제했을 때, 새로운 카테고리를 추가했을 때 먼저 화면상의 렌더링을 처리한 뒤, 바뀐 데이터를 가지고 localStorage를 다시 setItem하는 방식으로 처리했습니다.

  • 동적 렌더링

    하드코딩이 아닌 동적 렌더링을 위해 저는 template 태그 라는 것을 사용했습니다. 틀을 짜놓고 원하는 콘텐츠만 바꾸고 싶을 때 사용하기 제격입니다.
    html 파일을 보면 다음과 같이 template을 정의해놓았습니다.

      <template id="item-template">
          <li id="{item-id}">
          <div class="title">
              <small>{item-category}</small>
              <h3>{item-name}</h3>
          </div>
          <h4 class="text-{item-type}">{item-cost}원</h4>
          <button class="delete-btn" type="button">X</button>
          </li>
      </template>

    여기서 가변적인 콘텐츠인 {item-category}, {item-name}등을 중괄호 형식으로 지정해놓고, 이 부분을 javascript를 통해 바꿔치기 하는 것입니다.

    /* list를 탐색하면서 요소를 하나씩 가계부 내역으로 렌더링하는 함수 */
      function renderList(list) {
          const listWrapper = document.getElementById("list-wrapper"); //리스트가 들어갈 부모 노드
          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);
    
              itemContent.innerHTML = itemNewHtml; //새롭게 바뀐 html을 템플릿에 적용
              listWrapper.appendChild(itemContent.content); //부모노드 안에 넣기
          });
      }

    템플릿 노드를 가져와서 이를 복사하고, 이 안의 innerHTML을 복사한 뒤 해당 innerHTML에서 바꿔야할 부분을 replace() method를 통해 변경합니다. 그리고 이 변경된 내용을 아까 복사한 템플릿에 다시 넣은 뒤 appendChild로 부모 노드에 달아주는 것이죠.

  • 나의 자산, 총수입/총지출 계산

    나의 자산, 총수입, 총지출을 계산하는 함수는 다음과 같습니다.

    function countBalance(list) {
      balance = INIT_BALANCE;
      income = INIT_BALANCE;
      spending = INIT_BALANCE;
    
      list.forEach((item) => {
          balance += item.cost;
          item.cost < 0 ? (spending += item.cost) : (income += item.cost);
      });
    
      const balanceNode = document.getElementById("balance");
      balanceNode.innerText = balance.toLocaleString() + "원";
    
      const incomeNode = document.getElementById("income-text");
      incomeNode.innerText = income.toLocaleString() + "원";
    
      const spendingNode = document.getElementById("spending-text");
      spendingNode.innerText = spending.toLocaleString() + "원";
    }

    리스트를 탐색하면서 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과 관련된 처리까지 하고 있습니다. 이 부분은 수정이 필요합니다.

    • select box option 동적 렌더링
      /* select box 옵션을 동적으로 변경하는 함수 */
      function handleSelectOption() {
          const currentOption = addFormRadioIncome.checked
          ? incomeCategoryData
          : spendingCategoryData;
      
          addFormSelect.replaceChildren();
          currentOption.forEach((category) => {
          const option = document.createElement("option");
          option.text = category;
          option.value = category;
          addFormSelect.add(option);
          });
      }
      수입/지출에 대한 각각의 카테고리도 localStorage에 저장되어있습니다. 따라서 이 값을 가져와서 forEach()를 돌려 해당 값을 innerText로 하는 <option> 태그를 만들고 <select>태그의 자식으로 넣어줍니다.
    • 입력한 내용으로 새로운 항목 추가
      /* 리스트 아이템 등록을 동작시키는 함수 */
      ``function handleItemAdd(
      list,
      addFormSelect,
      costInput,
      nameInput,
      addFormRadioIncome
      ) {
      if (!addFormSelect.value || !costInput.value || !nameInput.value) {
          alert("모든 항목을 입력해 주세요.");
      } else {
          //validation 통과
          const newItem = {
          customId: list[list.length - 1].customId + 1, //현재 리스트의 마지막 요소의 customId의 다음 번호로 새로운 customId를 정함
          category: addFormSelect.value,
          name: nameInput.value,
          cost: addFormRadioIncome.checked
              ? Number(costInput.value.replaceAll(",", ""))
              : -1 * Number(costInput.value.replaceAll(",", "")),
          };
          alert("새로운 내역이 추가되었습니다.");
          list.push(newItem);
          localStorage.setItem("list_data", JSON.stringify(list));
      
          countBalance(list);
          renderList(list);
      }
      handleItemDeleteModal(list);
      }``
      먼저 form에서 요구하는 세가지 값에 대한 validation을 확인한 뒤, 문제가 없다면 newItem이라는 상수에 새로 추가될 아이템을 만들어줍니다. 이 때 customId는 현재 list에 있는 마지막 항목의 customId에 +1을 한 값입니다. 또한 cost에 대해서, 입력 할 때 지출 금액이라고해서 앞에 -를 붙여주는 게 아니기 때문에, 제가 설정한 데이터 형식상 지출 카테고리일 시 음수로 처리를해서 넣어주어야 합니다. 이에 대한 삼항연산자 처리를 해주었습니다.
      addFormRadioIncome.checked ? Number(costInput.value.replaceAll(",", "")) : -1 * Number(costInput.value.replaceAll(",", ""))

🥺 소요 시간, 어려웠던 점

  • 8h
  • 함수를 어떻게 나눠서 정의하고, 언제 어디서 어떻게 호출해야하는지에 대한 처리 순서가 가장 어려운 것 같습니다. 그러니까 항목을 추가하는 함수를 실행하고 나면, 항목을 삭제하는 동작에 대한 함수를 다시 한번 호출해주어야 연속적으로 동작이 계속되거든요. 근데 그렇다고 호출을 남발하면 recursion이 발생하여 잘못된 삭제나 추가가 일어납니다. 이와 똑같은 문제를 32기 때도 겪었는데, 그 때 모달을 열고 닫는 함수모달 안에서의 동작을 처리하는 함수를 명확히 분리함으로써 해결했었거든요. 그래서 이번에도 그런 방향으로 접근을 했는데, 시간을 많이 쓰지 못해서... 조금씩 에러가 나기도 합니다. ㅠㅠ

🌈 구현 결과물

배포링크

  • 내역 등록 및 삭제

default.mov
  • 수입/지출 필터링

default.mov
  • 카테고리 추가

default.mov

@simeunseo simeunseo self-assigned this Oct 27, 2023
@simeunseo simeunseo marked this pull request as ready for review October 27, 2023 15:00
Copy link
Member

@qwp0 qwp0 left a comment

Choose a reason for hiding this comment

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

너무 잘하셔서 또 배워만 갑니다.. 저는 카테고리 추가만 로컬스토리지 활용했는데 리스트 목록까지 모두 로컬스토리지 이용해서 구현하셨네요😮 최고.. 3주차 과제도 화이팅해요 우리 ㅎㅎ

Comment on lines 17 to 34
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); //부모노드 안에 넣기
});
}
Copy link
Member

Choose a reason for hiding this comment

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

템플릿 태그 이용해서 훨씬 간단하게 할 수 있네요ㅠㅠ 배워갑니다!

</div>
<div>
<label>금액</label>
<input type="text" />
<input type="text" placeholder="숫자로 입력해 주세요" />
Copy link
Member

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("삭제한거 로컬에 반영함");
Copy link
Member

Choose a reason for hiding this comment

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

콘솔 창에 찍은 코드가 좀 남아있는데 확인차 남겨두신 걸까요?? 전 실수로 빼먹고 못지운 게 많아서 혹시나 하고 남겨봐요 ㅎㅎ

Comment on lines 54 to 88
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 = "";
}
});
}
Copy link
Member

Choose a reason for hiding this comment

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

카테고리 추가를 수입과 지출을 함수로 나눠서 하셨네요! 전 겹치는 코드가 많은 것 같아서 하나로 묶어서 했는데 나눠서 하는 것도 좋은 것 같습니당 함수 나누는 거 너무 어려워요ㅠㅠㅠ

Comment on lines 361 to 369
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;
}
Copy link
Member

Choose a reason for hiding this comment

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

hover시에 커서가 포인터로 바뀌면 좋을 것 같아요! 내역 추가 모달 창 안에 있는 저장하기 버튼이랑 닫기 버튼까지요!

Comment on lines +66 to +79

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);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

전 체크박스가 체크된 개수로 먼저 나눠서 if문 작성했는데 각각의 체크박스의 체크 여부로 구현한 은서님 코드가 훨씬 가독성이 좋은 것 같습니다!

Copy link

@eonseok-jeon eonseok-jeon left a comment

Choose a reason for hiding this comment

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

코드 잘 봤습니다!! 깔끔하게 잘 하신 거 같아요~~
많이 배우고 갑니다!
고생하셨습니다~~ :)

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>

Choose a reason for hiding this comment

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

우왕 이런 게 있었군요! 배워 갑니다 :)

Comment on lines +86 to +110
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);
}

Choose a reason for hiding this comment

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

수입이나 지출을 누른 후, 내역을 삭제하려고 하면 삭제가 되지 않는 오류가 있습니다!! 왜인지 정확히는 모르겠지만 아무래도 리스트가 변경되고 그 변경된 리스트에 대해서 삭제 모달창이 열리는 로직이 구현이 제대로 구현이 되지 않았지 않을까 라는 생각이 듭니다!

추가로 x 버튼에 cursor: pointer;가 적용되어 있지 않아요

Comment on lines +32 to +37
/* Reapply the pointer cursor for anchor tags */
a,
button {
cursor: revert;
}

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로 수정하는 것도 하나의 방법이 될 거 같아요!

Comment on lines +6 to +8
let listData = []; //localStorage에 저장된 가계무 목록을 가져와 저장하는 배열
let incomeCategoryData = []; //localStorage에 저장된 수입 카테고리 목록을 가져와 저장하는 배열
let spendingCategoryData = []; //localStorage에 저장된 지출 카테고리 목록을 가져와 저장하는 배열

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으로 선언 & 재할당)

Copy link
Member

@seobbang seobbang left a comment

Choose a reason for hiding this comment

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

전체적으로 로직이 변경 사항에 대해서만 다시 계산하거나 변경하는 로직이 아니라, 하나가 변경되면 다시 처음부터 모든 것을 계산하는 방식으로 구현이 된 것 같아용(ex. countBalance) 그럼 아무래도 비효율적이겠죠?! 항상 처음부터 다시 계산하기 보다는 변경된 사항을 어떻게 반영할 수 있을지 로직을 세우면 더 좋을 것 같습니다 !
➕ 코드가 넘 길어서 JS 파일 분리를 하는 것도 좋아보여요!

심화 과제까지 고생 넘 많으셨습니당 👍

Comment on lines +92 to +104
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);
}
}
Copy link
Member

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 적으로 관리하는 방법도 좋을 것 같아용

Comment on lines +126 to +137
deleteModalCancelBtn.addEventListener("click", () => {
deleteModal.style.display = "none";
targetElement = null;
});

deleteModalConfirmBtn.addEventListener("click", () => {
deleteModal.style.display = "none";

targetElement && handleItemDelete(list, targetElement);
});
});
});
Copy link
Member

Choose a reason for hiding this comment

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

deleteModalCanceBtn과 deletemodalConfirmBtn은 deleteBtns를 순회하는 동안 계속해서 addEventListener를 붙여주는건가욤?

Comment on lines +120 to +124
deleteBtns.forEach((btn) => {
btn.addEventListener("click", (e) => {
let targetElement = e.target.parentElement;

deleteModal.style.display = "flex";
Copy link
Member

Choose a reason for hiding this comment

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

이벤트 위임을 사용하면 반복문을 돌려서 일일히 이벤트리스너를 붙여주지 않아도 됩니다!

Comment on lines +17 to +26
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)
);
Copy link
Member

Choose a reason for hiding this comment

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

이거 category/script.js 에서도 똑같이 쓰이는 것 같은데 따로 함수화해서 export해서 써도 좋겠다!

Comment on lines +66 to +78
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);
Copy link
Member

Choose a reason for hiding this comment

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

오호! template이라는 신기한 개념을 사용했네

@simeunseo simeunseo merged commit 2787862 into main Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants