맥도날드 키오스크 앱 썸네일
# 1. 구상
** 과제의 기본 요구사항 **
------------------------------------------------------------------------------------------------
‘햄버거 주문서’ 를 클릭하면 classList.toggle() 메서드를 통해 ‘추가’, ‘제거’ 버튼이 토글 됩니다.
햄버거 아이템 리스트 배열을 생성해줍니다.
추가 버튼을 클릭하면 배열 리스트 1개가 추가 됩니다.
이때 배열 리스트의 값은 추가 버튼 누를 때 마다 숫자가 1개씩 증가 됩니다.
제거 버튼을 클릭하면 배열 리스트의 값 중 마지막 값 1개가 제거됩니다.
🔴 : 다른 방식으로 구현
🟢 : 요구사항에 만족
🔵 : 응용하여 구현
-------------------------------------------------------------------------------
하루안에 구현해야했기 때문에, 직접 디자인을 하고 마크업까지 하려면 지금 속도로는 도저히 불가능해 보였다.
그래서, UI 레퍼런스를 참고하여 클론코딩을 하는 방식을 채택했다.
McDonald's Kiosk Redesign - Reference | Food kiosk, Kiosk, Veggie world
아직 댓글이 없습니다! 대화를 시작하려면 하나를 추가하세요.
kr.pinterest.com
https://kr.pinterest.com/pin/879468633465698736/
매우 조잡하지만 어쨌든 도움은 된다 ㅋㅋ
화면 구현을 할 때 마크업 구조를 처음부터 잘못짜서
레이아웃이 망가지는 쓴 맛을 본 경험이 여러번 있기 때문에 새로운 습관을 들이기로 했다.
바로.. 그림으로 컨테이너 박스를 그룹단위로 그려보는 것이다.
그려보면서 느낀 것은,
확실히 도움은 되었지만, "좋은 프로그램 놔두고 굳이 아날로그로 해야하나?"
라는 인팁 특유의 "굳이" 화법이 발동되었다.
다음번엔 피그마도 배워서 직접 디자인까지 해봐야겠다. (재밌겠다..)
레퍼런스 참고 + 추가될 기능을 예상하여 구현한 결과물
참고한 디자인 말고도 나름 욕심이 생겨서 인트로와 주문완료 화면도 추가해주었다.
또, 모바일 앱처럼 만들고 싶었기 때문에 가지고있는 아이폰의 다이나믹 아일랜드도 CSS로 디자인해보았다.
(까매서 잘 안보이지만, 실제 화면을 자세히보면 전면 카메라 렌즈가 반사되는 듯한 디테일도 신경썼다..)
**메인 컨테이너에서 오버플로우 시킨 이유는 슬라이드 형식으로 구현하고 싶어서이다.**
# 1. 우선 각 section의 부모인 ".mb-display"에게 absolute를 적용
.mb-display {
display: flex;
flex-direction: row;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: 1s left ease-out;
}
# 2. section 공통 레이아웃 (section 3개를 복붙하면 ".mb-display"를 overflow 해버림)
/* 섹션 공통 */
.section {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
/* 섹션 공통 */
# 3. 그 후, 메인 컨테이너에 "overflow: hidden;" 을 적용하면...
#container {
width: 100%;
max-width: 400px;
max-height: 732px;
aspect-ratio: 215 / 466;
position: relative;
border: 6px solid var(--black);
border-radius: 24px;
box-shadow: 0 0 30px 30px rgba(0, 0, 0, 0.372) inset;
background-color: white;
overflow: hidden;
}
# 4. 예쁘게 마스킹된다!
메인 컨테이너에 overflow: hidden을 적용한 모습
# 5. 그리고 ".mb-display"의 left의 값을 js로 조작하면,
간단하게 이미지 슬라이드 완성!......
# 2. 기능 구현 시작
......이 될줄 알았지만, 버튼에 이벤트리스너를 등록하고 직접 작동해보니,
left값이 클릭하는대로 늘어나버려서 뷰포인트를 넘어 우주 끝까지 가버릴 기세였다.
(어디가니..)
그래서 버그를 해결해야 하는데..
예외처리하는 로직이 생각외로 머리아팠다.
마땅한 방법이 떠오르지 않는다면, 일단 변수부터 지정해보자.
// 디스플레이 개별 넓이
let displayLocationX = 0;
const displayWidth = 388;
디스플레이의 초기 좌표 : let displayLocationX = 0;
각 섹션 마다의 width 값 : displayWidth = 388;
일단, 디자인으로부터 예상되는 작동방식을 대략 정리하면,
----------------------------------------------------------------------------------------------------------------------
1. 화면이 section-1(intro) 인 상태에서는 "이전" 버튼이 비활성화 되어야 한다.
2. 주문 화면으로 넘어간 후, 정상적인 주문을 해야만 section-3(주문 완료 페이지)로 넘어가야 한다.
3. 주문 완료페이지로 넘어간 후, "다음" 버튼이 비활성화 되어야 한다.
** 정상적인 주문이란? :
1. "<ul>" 컨테이너에 "<li>" 가 제대로 추가되었을 경우,
2. Total 금액이 0원 이상일 경우
-------------------------------------------------------------------------------------------------------------------------------------
위 로직을 바탕으로 코딩을 해보자
// 디스플레이 개별 넓이
let displayLocationX = 0;
const displayWidth = 388;
// 슬라이드 다음 버튼
const btnCtrlNext = function () {
// 주문을 넣기전 또는 total금액이 0원 이상일 때 까지는 비활성화
if(displayLocationX === -displayWidth || displayLocationX === -displayWidth*2) {
displayLocationX = -displayWidth;
} else {
displayLocationX -= displayWidth;
pages.style.left = `${displayLocationX}px`;
}
}
// 슬라이드 이전 버튼
const btnCtrlPrev = function () {
if(displayLocationX === 0) {
displayLocationX = 0;
} else {
displayLocationX += displayWidth;
pages.style.left = `${displayLocationX}px`;
}
}
// 슬라이드 이벤트 리스너 등록
nextBtn.addEventListener('click', btnCtrlNext);
prevBtn.addEventListener('click', btnCtrlPrev);
정상적으로 작동한다. (짜릿함)
기세를 타서 바로 다음 기능을 구현해보자.
<ul> 컨테이너에 <li> 아이템을 추가하는 로직은 toDoList를 만들면서 배운 것을 그대로 적용하면 될 것 같아보였다.
1. 우선 상품 데이터를 담을 배열부터 만들자
const data = []; // 메뉴에서 선택되어 MyOrder에 추가될 list 객체를 담을 배열
2. 그리고 이벤트 리스너에 넣을 함수를 작명부터 하고, 이름을 토대로 구현할 기능을 생각해본다.
const addMenu = function () {}
3. 구현하고 싶은 작동 방식 :
- 메뉴 그리드를 클릭함
- 그리드의 상품 정보를 받아와서 <ul> 컨테이너에 <li> 로 받아온 데이터를 DOM에 추가
- <li>에 있는 수량 추가 버튼을 누를때마다 원본 데이터에 바로 반영
- 만약 수량이 0이 되고 "-"버튼을 한 번 더 누르면 cofirm창에 삭제하시겠습니까? 띄운 후 삭제.
4. 생각한 로직을 토대로 코딩 :
const addMenu = function () {
// 메뉴 정보를 담을 객체를 생성
const menu = {
id: Date.now(),
name: '',
cost: 0,
count: 1,
img: '',
};
// 메뉴 정보를 객체에 담음
menu.name = menuName.innerText.trim();
menu.cost = Number(cost.innerText);
menu.img = menuImg.src;
// 배열에 push
data.push(menu);
// 리스트 추가할 My Order 컨테이너 불러오기
const myOrderContainer = document.querySelector('.order-list');
// 리스트 요소 생성
const myOrderList = document.createElement('li');
const image = document.createElement('img');
const burgerName = document.createElement('p');
const burgerCost = document.createElement('p');
const countBtnContainer = document.createElement('div');
const subtractBtn = document.createElement('button');
const addBtn = document.createElement('button');
const numberCounter = document.createElement('span');
// 요소에 내용 추가
image.src = menu.img;
burgerName.innerText = menu.name;
burgerCost.innerText = menu.cost + '원';
numberCounter.innerText = menu.count;
subtractBtn.innerText = '-';
addBtn.innerText = '+';
// 생성한 리스트 요소에 스타일이 필요하면 클래스 추가
countBtnContainer.classList.add('count-btn');
subtractBtn.classList.add('subtract-count');
addBtn.classList.add('add-count');
numberCounter.classList.add('count-num');
// 리스트 요소 DOM에 추가
countBtnContainer.appendChild(subtractBtn);
countBtnContainer.appendChild(numberCounter);
countBtnContainer.appendChild(addBtn);
myOrderList.appendChild(image);
myOrderList.appendChild(burgerName);
myOrderList.appendChild(burgerCost);
myOrderList.appendChild(countBtnContainer);
myOrderContainer.appendChild(myOrderList);
// 클릭된 현재요소의 id를 finalOrderId에 할당 (index를 찾기 위함)
let finalOrderId = menu.id;
// 햄버거 수량 추가 버튼 이벤트리스너
addBtn.addEventListener('click', () => {
const itemIndex = data.findIndex((item) => item.id === finalOrderId);
if (itemIndex > -1) {
data[itemIndex].count++;
numberCounter.innerText = data[itemIndex].count;
}
});
// 햄버거 수량 감소 버튼 이벤트리스너
subtractBtn.addEventListener('click', () => {
const itemIndex = data.findIndex((item) => item.id === finalOrderId);
if (itemIndex > -1) {
// 햄버거 수량이 0이되면 리스트에서 제거하는 조건문
if(data[itemIndex].count < 1) {
let checkOrder = confirm(`주문 리스트에서 ${data[itemIndex].name}을 제외하시겠습니까?`);
if(checkOrder) {
data.splice(itemIndex, 1); // 원본배열에서 해당 인덱스 제거 후 앞으로 땡김.
myOrderList.remove(); //DOM의 MyOrderList에서 제거
}
} else {
data[itemIndex].count--;
numberCounter.innerText = data[itemIndex].count;
}
}
})
}
5. 코딩 하면서 가장 어려웠던 점 : 선택된 요소를 배열에서 삭제하기.
객체에 데이터를 넣고, 배열에 push하는 것 까지는 toDoList처럼 구현하면 되었기 때문에 간단했다.
하지만, 문제는 삭제하려고 하는 선택 된 객체의 index값을 어떻게 받아오느냐였다.
해결하기위해 관련 배열에 사용하는 메서드를 검색하던 도중,
findIndex() 를 알게되었고 메서드에 넣을 함수를 어떻게 짜야할지 생각해보았다.
1. 이 메서드는 true면 해당 index값을 반환하고 false면 -1을 반환하므로
2. 비교할 대상으로 객체에 미리 id값을 추가해보아야겠다고 생각했다.
3. 배열에 push하기 전, menu 객체에
id: Date.now(), 를 추가해주었고 그 다음 배열에 push하도록 했다.
4. 그 후, 현재 만들어진 객체의 id에 접근하여 id의 값을 함수가 실행되는 동안에 변수에 미리 저장해둔다.
5. 현재 이벤트리스너로 클릭된 객체의 id값과 배열에 담긴 객체의 id값은 true가 되므로,
현재 클릭된 대상의 index 번호를 findIndex() 로 받아올 수 있게된다!
#결과물.
배열 인덱스를 조회한 후 현재 선택된 요소를 삭제하는 어려움을 극복한 뒤에는 순조로웠다.
# 결제버튼 추가 > reduce()를 사용하여 누적산 > 값 출력
#github
https://morirokim.github.io/mac-order-app/
Document
주문 완료! 결제는 배달기사님에게 요청해주세요
morirokim.github.io
#code
/*
// 만들면서 신경쓴 것:
// 1. overflow: hidden을 이용해서 라이브러리 없이 직접 슬라이드 구현해보기
// 2. 특정 조건에 의해 슬라이드버튼 비활성화 하기
// 3. 객체배열을 활용한 CRUD 구현해보기 (delete는 버거 갯수가 0이 되었을때 '-'버튼을 한번 더 누르면 됨)
// 4. 하루안에 디자인 80%, 기능 90% 정도는 완성하기
// 5. gpt에 의존하지않고 직접 생각하고 문법이 기억나지 않으면 구글링해보기
// 6. console에 에러 안뜨게 여러 값을 넣어보고 디버깅 해보기
*/
const prevBtn = document.querySelector('.prev');
const nextBtn = document.querySelector('.next');
const addCountBtn = document.querySelector('.add-count');
const subtractBtn = document.querySelector('.subtract-count');
const countNum = document.querySelector('.count-num');
const menuItem = document.querySelector('.grid-item');
const menuImg = document.querySelector('.menu-img');
const menuName = document.querySelector('.burger-name');
const cost = document.querySelector('.cost');
const payBtn = document.querySelector('.pay-btn');
// total 금액 표시하는 요소 불러오기
const total = document.querySelector('.total-price');
const pages = document.querySelector('.mb-display');
// 디스플레이 개별 넓이
let displayLocationX = 0;
const displayWidth = 388;
const data = []; // 메뉴에서 선택되어 MyOrder에 추가될 list 객체를 담을 배열
const addMenu = function () {
// 메뉴 정보를 담을 객체를 생성
const menu = {
id: Date.now(),
name: '',
cost: 0,
count: 1,
img: '',
};
// 메뉴 정보를 객체에 담음
menu.name = menuName.innerText.trim();
menu.cost = Number(cost.innerText);
menu.img = menuImg.src;
// 배열에 push
data.push(menu);
// 리스트 추가할 My Order 컨테이너 불러오기
const myOrderContainer = document.querySelector('.order-list');
// 리스트 요소 생성
const myOrderList = document.createElement('li');
const image = document.createElement('img');
const burgerName = document.createElement('p');
const burgerCost = document.createElement('p');
const countBtnContainer = document.createElement('div');
const subtractBtn = document.createElement('button');
const addBtn = document.createElement('button');
const numberCounter = document.createElement('span');
// 요소에 내용 추가
image.src = menu.img;
burgerName.innerText = menu.name;
burgerCost.innerText = menu.cost + '원';
numberCounter.innerText = menu.count;
subtractBtn.innerText = '-';
addBtn.innerText = '+';
// 생성한 리스트 요소에 스타일이 필요하면 클래스 추가
countBtnContainer.classList.add('count-btn');
subtractBtn.classList.add('subtract-count');
addBtn.classList.add('add-count');
numberCounter.classList.add('count-num');
// 리스트 요소 DOM에 추가
countBtnContainer.appendChild(subtractBtn);
countBtnContainer.appendChild(numberCounter);
countBtnContainer.appendChild(addBtn);
myOrderList.appendChild(image);
myOrderList.appendChild(burgerName);
myOrderList.appendChild(burgerCost);
myOrderList.appendChild(countBtnContainer);
myOrderContainer.appendChild(myOrderList);
// 클릭된 현재요소의 id를 finalOrderId에 할당 (index를 찾기 위함)
let finalOrderId = menu.id;
// 햄버거 수량 추가 버튼 이벤트리스너
addBtn.addEventListener('click', () => {
const itemIndex = data.findIndex((item) => item.id === finalOrderId);
if (itemIndex > -1) {
data[itemIndex].count++;
numberCounter.innerText = data[itemIndex].count;
totalPrice();
}
});
// 햄버거 수량 감소 버튼 이벤트리스너
subtractBtn.addEventListener('click', () => {
const itemIndex = data.findIndex((item) => item.id === finalOrderId);
if (itemIndex > -1) {
// 햄버거 수량이 0이되면 리스트에서 제거하는 조건문
if(data[itemIndex].count < 1) {
let checkOrder = confirm(`주문 리스트에서 ${data[itemIndex].name}을 제외하시겠습니까?`);
if(checkOrder) {
data.splice(itemIndex, 1); // 원본배열에서 해당 인덱스 제거 후 앞으로 땡김.
myOrderList.remove(); //DOM의 MyOrderList에서 제거
totalPrice();
}
} else {
data[itemIndex].count--;
numberCounter.innerText = data[itemIndex].count;
totalPrice();
}
}
})
totalPrice();
}
// 지불 버튼 함수
const pay = function () {
//totalPrice함수를 재사용하여 반환값을 받아옴
const payAuthorization = totalPrice();
// 햄버거가 My Order list에 있고, payAuthorization의 값이 0원 이상일때 활성화
if(data !== null && payAuthorization > 0) {
displayLocationX -= displayWidth;
pages.style.left = `${displayLocationX}px`;
}
}
// 총 가격 계산해주는 함수 addMenu() 에서 쓰임
const totalPrice = function () {
let calcResult = data.reduce((acc, curr) => {
return acc + (curr.cost * curr.count);
}, 0);
total.innerText = calcResult + '원';
return calcResult;
}
// 슬라이드 다음 버튼
const btnCtrlNext = function () {
// 주문을 넣기전 또는 total금액이 0원 이상일 때 까지는 비활성화
if(displayLocationX === -displayWidth || displayLocationX === -displayWidth*2) {
displayLocationX = -displayWidth;
} else {
displayLocationX -= displayWidth;
pages.style.left = `${displayLocationX}px`;
}
}
// 슬라이드 이전 버튼
const btnCtrlPrev = function () {
if(displayLocationX === 0) {
displayLocationX = 0;
} else {
displayLocationX += displayWidth;
pages.style.left = `${displayLocationX}px`;
}
}
// 슬라이드 이벤트 리스너 등록
nextBtn.addEventListener('click', btnCtrlNext);
prevBtn.addEventListener('click', btnCtrlPrev);
// 메뉴 그리드 이벤트 리스너 등록
menuItem.addEventListener('click', addMenu);
// 지불 버튼 이벤트 리스너 등록
payBtn.addEventListener('click', pay);