moonbucks-menu 스탭3 회고
- -
블랙커피 스터디 소개
블랙커피 스터디를 소개합니다. 아래 링크를 클릭해서 확인할수 있습니다 :)
moonbucks-menu 스탭3 회고
안녕하세요 TriplexLab 입니다.
오늘은 moonbucks-menu 스탭2을 이어서 스탭3 회고를 작성해보겠습니다.
moonbucks-menu 스탭1, 2 회고 참고해주세요. 👍👍
👉 스탭 3 중간과정에서 얻은 인사이트
1. MenuApi 객체를 따로 분리해서 서버와의 통신 로직을 구현합니다.(서버와 통신 CRUD작업 구현)
2. fetch 비동기 api를 사용하는 부분을 async await을 사용하여 구현한다.
// moonbucks-menu/src/js/index.js
// 웹 서버를 띄운다.
// [x]서버에 새로운 메뉴명이 추가될수 있도록 요청한다.
// [x]서버에 카레고리별 메뉴리스트를 불러와서 화면에 그려준다.
// [x]서버에 메뉴가 수정 될 수 있도록 요청한다.
// [x]서버에 메뉴의 품절상태가 토글될 수 있도록 한다.
// [x]서버에 메뉴가 삭제 될 수 있도록 요청한다.
// 리팩토링 부분
// localStorage에 저장하는 로직은 지운다.
// fetch 비동기 api를 사용하는 부분을 async await을 사용하여 구현한다.
// 사용자 경험
// API 통신이 실패하는 경우에 대해 사용자가 알 수 있게 alert으로 예외처리를 진행한다.
// 중복되는 메뉴는 추가할 수 없다.
import {$} from './utils/dom.js'
const BASE_URL = 'http://localhost:3000/api';
const MenuApi = {
async getAllMenuByCategory(category) {
const res = await fetch(`${BASE_URL}/category/${category}/menu`)
return res.json();
},
async createMenu(category, name){
const res = await fetch(`${BASE_URL}/category/${category}/menu`,{
method: 'POST',
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify({name})
})
if(!res.ok){
console.log('에러가 발생했습니다.')
}
},
async updatemenu(category, name, menuId) {
const res = await fetch(`${BASE_URL}/category/${category}/menu/${menuId}`, {
method: 'PUT',
headers : {
'Content-Type':'application/json'
},
body: JSON.stringify({name})
})
if(!res.ok){
console.log('에러가 발생했습니다.')
}
return res.json();
},
async toggleSoldOutMenu(category, menuId){
const res = await fetch(`${BASE_URL}/category/${category}/menu/${menuId}/soldout`,{
method: 'PUT',
});
if(!res.ok){
console.error('에러가 발생했습니다.')
}
},
async deleteMenu(category, menuId){
const res = await fetch(`${BASE_URL}/category/${category}/menu/${menuId}`,{
method: 'DELETE',
});
if(!res.ok){
console.error('에러가 발생했습니다.')
}
}
}
function App() {
const menuForm = $('#menu-form');
const idMenuName = $('#menu-name');
const submitBtn = $('#menu-submit-button');
const menuList = $('#menu-list');
this.menu = {
espresso: new Array(),
frappuccino: new Array(),
blended: new Array(),
teavana: new Array(),
desert: new Array()
};
this.currentCategory = 'espresso';
this.init = async () => {
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
render();
initEventListeners();
};
const render = () => {
const template = this.menu[this.currentCategory].map((item) => {
return`<li data-menu-id='${item.id}' class="menu-list-item d-flex items-center py-2">
<span class="w-100 pl-2 menu-name ${item.isSoldOut ? 'sold-out' : ''}">${item.name}</span>
<button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-sold-out-button"> 품절 </button>
<button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-edit-button"> 수정 </button>
<button type="button" class="bg-gray-50 text-gray-500 text-sm menu-remove-button"> 삭제 </button>
</li>`;
}).join('');
menuList.innerHTML = template;
commonUpdateMenuCount();
};
const commonUpdateMenuCount = () => {
const menuCount = this.menu[this.currentCategory].length;
$('.menu-count').innerText = `총 ${menuCount} 개`;
};
const commonAddMenuName = async () => {
if(idMenuName.value === ''){
alert('값을 입력해주세요.');
return;
}
const menuName = idMenuName.value;
await MenuApi.createMenu(this.currentCategory, menuName);
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
render();
idMenuName.value = "";
};
const updateMenuName = async (e) => {
const menuId = e.target.closest('li').dataset.menuId;
const menuName = e.target.closest('li').querySelector('.menu-name');
const updateMenuName = prompt('메뉴명을 수정하세요', menuName.innerText);
await MenuApi.updatemenu(this.currentCategory, updateMenuName, menuId);
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
render();
};
const removeMenuName = async(e) => {
const menuId = e.target.closest('li').dataset.menuId;
if(confirm('정말 삭제하시겠습니까?')){
await MenuApi.deleteMenu(this.currentCategory, menuId)
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
render();
}
};
const soldOutMenu = async (e) => {
const menuId = e.target.closest('li').dataset.menuId;
await MenuApi.toggleSoldOutMenu(this.currentCategory, menuId)
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
render();
};
const initEventListeners = () => {
menuList.addEventListener('click', (e) => {
if(e.target.classList.contains('menu-edit-button')){
updateMenuName(e);
return;
}
if(e.target.classList.contains('menu-remove-button')){
removeMenuName(e);
return;
}
if(e.target.classList.contains('menu-sold-out-button')){
soldOutMenu(e);
return;
}
});
menuForm.addEventListener('submit', (e) => {
e.preventDefault();
});
submitBtn.addEventListener('click', commonAddMenuName);
idMenuName.addEventListener('keypress', (e) => {
if(e.key !== 'Enter') return;
commonAddMenuName();
});
$('nav').addEventListener('click', async (e) => {
const isCategoryBtn = e.target.classList.contains('cafe-category-name');
if(isCategoryBtn){
const catagoryName = e.target.dataset.categoryName;
this.currentCategory = catagoryName;
$('#category-title').innerText = `${e.target.innerText} 메뉴 관리`;
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
render();
}
});
};
}
const app = new App();
app.init('menu');
중간 까지 작업한 파일을 공유합니다. 😃😃
👉 스탭 3에서 얻은 인사이트
1. API 파일 따로 만들어서 진행하므로써 코드의 가독성이 좋아짐.
2. 비동기 처리할때 순서를 보장해주기위해서 async, await를 사용합니다.
3. 서버 요청 할 때 option 객체(POST, PUT, DELETE) 분리한다.하므로써 코드의 중복을 제거합니다.
4. request 함수따라(응답 데이터가 있을때랑, 없을때를 구분) 분리한다.
// src/js/api/index.js
const BASE_URL = "http://localhost:3000/api";
const HTTP_METHOD = {
POST(data){
return {
method: 'POST',
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify(data)
}
},
PUT(data){
return{
method: 'PUT',
headers: {
'Content-Type':'application/json'
},
body: data ? JSON.stringify(data) : null
}
},
DELETE(){
return {
method: 'DELETE'
}
}
}
const request = async (url, option) => {
const res = await fetch(url, option);
if(!res.ok){
alert('에러가 발생했습니다.')
console.log(e);
}
return res.json();
};
const requestWithoutJson = async(url, option) => {
const res = await fetch(url, option);
if(!res.ok){
alert('에러가 발생했습니다.')
console.log(e);
}
return res;
}
const MenuApi = {
async getAllMenuByCategory(category) {
return request(`${BASE_URL}/category/${category}/menu`);
},
async createMenu(category, name) {
return request(`${BASE_URL}/category/${category}/menu`, HTTP_METHOD.POST({name}))
},
async updatemenu(category, name, menuId){
return request(`${BASE_URL}/category/${category}/menu/${menuId}`, HTTP_METHOD.PUT({name}))
},
async toggleSoldOutMenu(category, menuId){
return request(`${BASE_URL}/category/${category}/menu/${menuId}/soldout`, HTTP_METHOD.PUT())
},
async deleteMenu(category, menuId) {
return requestWithoutJson(`${BASE_URL}/category/${category}/menu/${menuId}`, HTTP_METHOD.DELETE())
}
}
export default MenuApi
// src/js/index.js
// TODO step3 서버 요청 부분
// [x]웹 서버를 띄운다.
// [x]서버에 새로운 메뉴명이 추가될 수 있도록 요청한다.
// [x]서버에 카테고리별 메뉴리스트를 불러온다.
// [x]서버에 메뉴가 수정 될 수 있도록 요청한다.
// [x]서버에 메뉴의 품절상태를 변경(토글)할수 있도록 요청한다.
// [x]서버에 메뉴가 삭제될수 있도록 요청한다.
// TODO 리팩토링 부분
// [x]localStorage에 저장하는 로직은 지운다.
// [x]fetch 비동기 api를 사용하는 부분을 async await을 사용하여 구현한다.
// TODO 사용자 경험
// [x]API 통신이 실패하는 경우에 대해 사용자가 알 수 있게 alert으로 예외처리를 진행한다.
// [x]중복되는 메뉴는 추가할 수 없다.
import {$} from './utils/dom.js'
import MenuApi from './api/index.js';
function App() {
const menuForm = $('#menu-form');
const idMenuName = $('#menu-name');
const submitBtn = $('#menu-submit-button');
const menuList = $('#menu-list');
this.menu = {
espresso: new Array(),
frappuccino: new Array(),
blended: new Array(),
teavana: new Array(),
desert: new Array()
};
this.currentCategory = 'espresso';
this.init = async() => {
render();
initEventListeners();
};
const render = async() => {
this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
const template = this.menu[this.currentCategory].map((item) => {
return`<li data-menu-id='${item.id}' class="menu-list-item d-flex items-center py-2">
<span class="w-100 pl-2 menu-name ${item.isSoldOut ? 'sold-out' : ''}">${item.name}</span>
<button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-sold-out-button"> 품절 </button>
<button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-edit-button"> 수정 </button>
<button type="button" class="bg-gray-50 text-gray-500 text-sm menu-remove-button"> 삭제 </button>
</li>`;
}).join('');
menuList.innerHTML = template;
commonUpdateMenuCount();
};
const commonUpdateMenuCount = () => {
const menuCount = this.menu[this.currentCategory].length;
$('.menu-count').innerText = `총 ${menuCount} 개`;
};
const commonAddMenuName = async() => {
if(idMenuName.value === ''){
alert('값을 입력해주세요.');
return;
}
const duplicaterItem = this.menu[this.currentCategory].find(menuItem => menuItem.name === idMenuName.value)
if(duplicaterItem){
alert('이미 등록된 메뉴입니다. 다시 입력해주세요.');
idMenuName.value = "";
return;
}
const menuName = idMenuName.value;
await MenuApi.createMenu(this.currentCategory, menuName);
render();
idMenuName.value = "";
};
const updateMenuName = async(e) => {
const menuId = e.target.closest('li').dataset.menuId;
const menuName = e.target.closest('li').querySelector('.menu-name');
const updateMenuName = prompt('메뉴명을 수정하세요', menuName.innerText);
await MenuApi.updatemenu(this.currentCategory, updateMenuName, menuId);
render();
};
const removeMenuName = async(e) => {
const menuId = e.target.closest('li').dataset.menuId;
if(confirm('정말 삭제하시겠습니까?')){
await MenuApi.deleteMenu(this.currentCategory, menuId);
render();
}
};
const soldOutMenu = async(e) => {
const menuId = e.target.closest('li').dataset.menuId;
await MenuApi.toggleSoldOutMenu(this.currentCategory, menuId);
render();
};
const changeCategory = (e) => {
const isCategoryBtn = e.target.classList.contains('cafe-category-name');
if(isCategoryBtn){
const catagoryName = e.target.dataset.categoryName;
this.currentCategory = catagoryName;
$('#category-title').innerText = `${e.target.innerText} 메뉴 관리`;
render();
}
};
const initEventListeners = () => {
menuList.addEventListener('click', (e) => {
if(e.target.classList.contains('menu-edit-button')){
updateMenuName(e);
return;
}
if(e.target.classList.contains('menu-remove-button')){
removeMenuName(e);
return;
}
if(e.target.classList.contains('menu-sold-out-button')){
soldOutMenu(e);
return;
}
});
menuForm.addEventListener('submit', (e) => {
e.preventDefault();
});
submitBtn.addEventListener('click', commonAddMenuName);
idMenuName.addEventListener('keypress', (e) => {
if(e.key !== 'Enter') return;
commonAddMenuName();
});
$('nav').addEventListener('click', changeCategory);
};
}
const app = new App();
app.init('menu');
👉 작업 순서
1. BASE_URL 웹 서버 변수명 선언
2. 비동기 처리하는데 해당하는 부분이 어디인지 확인하고, 웹서버에 요청하게끔 코드 짜기
3. 서버에 요청한 후 데이터를 받아서 화면에 렌더링 하기
4.리팩터링
- LocalStorage 부분 지우기
- API 파일 따로 만들어서 진행
- 페이지 렌더링과 관련해서 중복되는 부분들 제거
- 서버 요청 할 때 option 객체(POST, PUT, DELETE) 분리한다.
- request 함수따라(응답 데이터가 있을때랑, 없을때를 구분) 분리한다.
- 카테고리 버튼 클릭 시 콜백함수 분리
5.사용자 경험 부분
- API 통신이 실패하는 경우에 대해 사용자가 알 수 있게 alert으로 예외처리를 진행한다.
- 중복되는 메뉴는 추가할 수 없다.
여기 까지 작업한 파일을 공유합니다. 😃😃
소중한 공감 감사합니다