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

[14기 김동영] step2 - 상태 관리로 메뉴 관리하기 #288

Open
wants to merge 14 commits into
base: pers0n4
Choose a base branch
from
Open
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@

## 🎯 step2 요구사항 - 상태 관리로 메뉴 관리하기

- [ ] [localStorage](https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage)에 데이터를 저장하여 새로고침해도 데이터가 남아있게 한다.
- [ ] 에스프레소, 프라푸치노, 블렌디드, 티바나, 디저트 각각의 종류별로 메뉴판을 관리할 수 있게 만든다.
- [ ] 페이지에 최초로 접근할 때는 에스프레소 메뉴가 먼저 보이게 한다.
- [ ] 품절 상태인 경우를 보여줄 수 있게, 품절 버튼을 추가하고 `sold-out` class를 추가하여 상태를 변경한다.
- [x] [localStorage](https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage)에 데이터를 저장하여 새로고침해도 데이터가 남아있게 한다.
- [x] 에스프레소, 프라푸치노, 블렌디드, 티바나, 디저트 각각의 종류별로 메뉴판을 관리할 수 있게 만든다.
- [x] 페이지에 최초로 접근할 때는 에스프레소 메뉴가 먼저 보이게 한다.
- [x] 품절 상태인 경우를 보여줄 수 있게, 품절 버튼을 추가하고 `sold-out` class를 추가하여 상태를 변경한다.
- 품절 상태 메뉴의 마크업

```js
Expand Down
14 changes: 7 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,33 @@ <h1 class="text-center font-bold">🌝 문벅스 메뉴 관리</h1>
<main class="mt-10 d-flex justify-center">
<div class="wrapper bg-white p-10">
<div class="heading d-flex justify-between">
<h2 class="mt-1">☕ 에스프레소 메뉴 관리</h2>
<h2 class="mt-1 category-title">☕ 에스프레소 메뉴 관리</h2>
<span class="mr-2 mt-4 menu-count">총 0개</span>
</div>
<form id="espresso-menu-form">
<form id="menu-form">
<div class="d-flex w-100">
<label for="espresso-menu-name" class="input-label" hidden>
<label for="menu-name" class="input-label" hidden>
에스프레소 메뉴 이름
</label>
<input
type="text"
id="espresso-menu-name"
id="menu-name"
name="espressoMenuName"
class="input-field"
placeholder="에스프레소 메뉴 이름"
placeholder="메뉴 이름"
autocomplete="off"
/>
<button
type="submit"
name="submit"
id="espresso-menu-submit-button"
id="menu-submit-button"
class="input-submit bg-green-600 ml-2"
>
확인
</button>
</div>
</form>
<ul id="espresso-menu-list" class="mt-3 pl-0"></ul>
<ul id="menu-list" class="mt-3 pl-0"></ul>
</div>
</main>
</div>
Expand Down
200 changes: 164 additions & 36 deletions src/js/App.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,199 @@
import { select } from "./dom.js";
import MenuItem from "./MenuItem.js";
import MenuItem from "./components/MenuItem.js";
import { select } from "./utils/dom.js";
import { localStore } from "./utils/storage.js";

export default function App() {
/** @type {HTMLFormElement} */
const menuForm = select("#espresso-menu-form");
export default class App {
/**
* @type {HTMLFormElement}
* @readonly
*/
#menuForm = select("#menu-form");

/**
* @type {HTMLUListElement}
* @readonly
*/
#menuList = select("#menu-list");

/** @type {HTMLUListElement} */
const menuList = select("#espresso-menu-list");
/**
* @type {HTMLInputElement}
* @readonly
*/
#menuInput = select("#menu-name");

/** @type {HTMLInputElement} */
const menuInput = select("#espresso-menu-name");
/**
* @type {HTMLSpanElement}
* @readonly
*/
#menuCount = select(".menu-count");

init();
/**
* @type {HTMLHeadingElement}
* @readonly
*/
#categoryTitle = select(".category-title");

function init() {
menuForm.addEventListener("submit", (event) => {
/**
* @typedef {"espresso" | "frappuccino" | "blended" | "teavana" | "desert"} MenuCategory
* @typedef {{name: string, isSoldOut: boolean}} MenuItem
* @typedef {{selectedCategory: MenuCategory, menuList: MenuItem[]}} State
*
* @type {State}
*/
#state = {
selectedCategory: "espresso",
menuList: localStore.get("espresso.menuList", []),
};

constructor() {
this.init();
this.render();
}

init() {
this.#menuForm.addEventListener("submit", (event) => {
event.preventDefault();
appendMenuItem();
this.appendMenuItem();
});

menuList.addEventListener("click", ({ target }) => {
if (!target) return;
this.#menuList.addEventListener("click", ({ target }) => {
if (!target) {
return;
}

const menuItem = target.closest("moon-menu-item");
const clickedIndex = Array.from(this.#menuList.children).indexOf(
menuItem,
);

if (target.classList.contains("menu-edit-button")) {
editMenuItem(menuItem);
this.editMenuItem(clickedIndex, menuItem);
} else if (target.classList.contains("menu-remove-button")) {
removeMenuItem(menuItem);
this.removeMenuItem(clickedIndex);
} else if (target.classList.contains("menu-sold-out-button")) {
this.toggleSoldOutMenuItem(clickedIndex, menuItem);
}
});

select("nav").addEventListener("click", ({ target }) => {
if (!target) {
return;
}

if ("categoryName" in target.dataset) {
/** @type {MenuCategory} */
const selectedCategory = target.dataset.categoryName;
this.setState({
selectedCategory,
menuList: localStore.get(`${selectedCategory}.menuList`, []),
});
this.updateCategoryTitle(target.innerText);
}
});
}

function appendMenuItem() {
if (menuInput.value.trim()) {
const menuItem = new MenuItem();
menuItem.setAttribute("name", menuInput.value.trim());
menuList.appendChild(menuItem);
menuInput.value = "";
updateMenuCount();
/**
* @param {State} nextState
*/
setState(nextState) {
this.#state = { ...this.#state, ...nextState };
this.render();
localStore.set(
`${this.#state.selectedCategory}.menuList`,
this.#state.menuList,
);
}

render() {
this.#menuList.replaceChildren(
...this.#state.menuList.map(
({ name, isSoldOut }) => new MenuItem(name, isSoldOut),
),
);
this.updateMenuCount();
}

appendMenuItem() {
const menuName = this.#menuInput.value.trim();
if (!menuName) {
return;
}

this.setState({
menuList: [...this.#state.menuList, { name: menuName, isSoldOut: false }],
});
this.#menuInput.value = "";
}

/**
* @param {number} index
* @param {MenuItem} menuItem
*/
function editMenuItem(menuItem) {
const newMenuName = window.prompt(
editMenuItem(index, menuItem) {
const menuName = window.prompt(
"수정할 메뉴 이름을 입력하세요",
menuItem.getAttribute("name"),
menuItem.name,
);
if (newMenuName) {
menuItem.setAttribute("name", newMenuName);
if (!menuName) {
return;
}

menuItem.name = menuName;
this.setState({
menuList: [
...this.#state.menuList.slice(0, index),
{
name: menuItem.name,
isSoldOut: menuItem.isSoldOut,
},
...this.#state.menuList.slice(index + 1),
Comment on lines +144 to +149

Choose a reason for hiding this comment

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

이 부분은 중복으로 사용되고 있는거 같은데 함수화 해서 사용하는건 어떨까요~?

],
});
}

/**
* @param {number} index
* @param {MenuItem} menuItem
*/
function removeMenuItem(menuItem) {
if (window.confirm("메뉴를 삭제하시겠습니까?")) {
menuItem.remove();
updateMenuCount();
toggleSoldOutMenuItem(index, menuItem) {
menuItem.isSoldOut = !menuItem.isSoldOut;
this.setState({
menuList: [
...this.#state.menuList.slice(0, index),
{
name: menuItem.name,
isSoldOut: menuItem.isSoldOut,
},
...this.#state.menuList.slice(index + 1),
],
});
}

/**
* @param {number} index
*/
removeMenuItem(index) {
if (!window.confirm("메뉴를 삭제하시겠습니까?")) {
return;
}

this.setState({
menuList: [
...this.#state.menuList.slice(0, index),
...this.#state.menuList.slice(index + 1),
],
});
}

updateMenuCount() {
const menuCount = this.#menuList.childElementCount;
this.#menuCount.textContent = `총 ${menuCount}개`;
}

function updateMenuCount() {
const menuCount = menuList.children.length;
select(".menu-count").textContent = `총 ${menuCount}개`;
/**
* @param {string} title
*/
updateCategoryTitle(title) {
this.#categoryTitle.textContent = `${title} 메뉴 관리`;
}
}
40 changes: 0 additions & 40 deletions src/js/MenuItem.js

This file was deleted.

Loading