메뉴를 클릭하면 장바구니에 추가할 지 물어보고 입력값에 따라 "추가", "취소" 처리하고 메뉴는 한 번에 1개만 담을 수 있음
장바구니에 담은 목록을 출력
주문 기능
장바구니에 담긴 모든 항목을 출력
합산하여 총 금액을 계산하고"주문하기"를 누르면 장바구니를 초기화
도전 LV1 구현
PriceFormatter클래스
package challenge.util;
public abstract class PriceFormatter {
public static String priceFormat(long price) {
DecimalFormat df = new DecimalFormat("#,###");
return df.format(price);
}
}
Kiosk 클래스와 ShoppingCart 클래스에서 사용할 한국 통화 포맷으로 포맷팅하는 유틸 추상 클래스를 작성하였으며 이후 LV2에서도 사용할 것이므로 challenge 하위의 util 패키지를 만들어서 보관하였다
객체 생성을 하지 못하도록 abstract 클래스로 작성하였으며 간편하게 사용할 수 있도록 static 메서드로 제공한다
ShoppingCart 클래스
package challenge.lv1;
import static challenge.util.PriceFormatter.priceFormat;
public class ShoppingCart {
private final Map<MenuItem, Integer> cart = new HashMap<>();
// 메서드 따로 설명
}
장바구니를 관리하기 위해 Map을 생성하고 구현체로 HashMap을 선택하였는데 보편적으로 Map자료 구조에서 HashMap이 성능이 빠르기도하고 특별히 연결 구조나 정렬이 필요한 트리구조가 필요한 것이 아니라면 일반적으로 HashMap을 선택한다고 하여 선정하였다
Map 자료구조를 선택한 이유는 MenuItem을 key 값으로 수량을 관리하여 장바구니의 아이템에 담긴 수량을 조절하기 위해 List가 아닌 Map으로 하였다.
보통 지금껏 예제에서 다뤄왔던 장바구니 구조에서는 아이템(MenuItem)이 수량을 가지도록 코드를 작성했었는데 여기에서는 기본 구조를 건들지 않고 장바구니를 추가해보고 싶어서 선택했다.
addCart()
public void addCart(MenuItem menuItem, int quantity) {
cart.put(menuItem, quantity);
System.out.println(menuItem.getName() + " "+ quantity + "개를 장바구니에 추가 하였습니다.");
}
장바구니인 자료구조에 키오스크에서 선택된 MenuItem과 수량을 저장하고 담긴 목록을 출력해준다.
수량을 입력할 때 잘못된 타입을 입력하면 0이 반환되고 의도적으로 0으로 수량을 입력하면 장바구니에 저장하지 않도록 예외를 처리한다.
showCart()
public String showCart() {
long totalPrice = 0;
System.out.println("[Orders]");
for (MenuItem menuItem : cart.keySet()) {
System.out.printf("%-18s | %d개 | 상품 합계: ₩ %10s | %s%n",
// itemPrice(): 수량이 반영된 개별 메뉴당 합계 가격
menuItem.getName(), cart.get(menuItem), priceFormat(itemPrice(menuItem)), menuItem.getDescription());
totalPrice += itemPrice(menuItem);
}
System.out.println();
System.out.println("[Total]");
String priceFormat = priceFormat(totalPrice);
System.out.println("₩ " + priceFormat);
return priceFormat;
}
장바구니의 내역을 cart.keySet()으로 Map의 key들을 Set 자료 구조로 뽑아서 하나씩 순회하면서 출력한다
그리고 itemPrice()메서드를 호출하여 상품별 합계 금액을 계한하고 PriceFormat.priceFormat()메서드로 포맷팅해서 출력한다
여기서 순회를 한 김에 전체 금액도 계산하는데 이때도 계산된 금액을 priceFormat()메서드로 포맷팅하여 출력하고 반환한다.
사실 여기 코드가 썩 마음에 들지 않는데, showCart()에서는 내역만 반환하고 전체 금액은 다른 별도의 메서드로 분리하려고 했으나 불필요한 멤버 변수가 사용되거나 반복문을 한번 더 돌게 되는 것 같아서 포기했다
구조상으로는 totalPrice는 별도의 메서드에서 관리하고 싶지만 현재 나의 능력으로는 지금과 같은 구조가 성능저하가 없을 것 같아서 이대로 진행 하였다
deleteAllCart()
public void deleteAllCart() {
cart.clear();
}
장바구니에 담긴 데이터를 싹 날리는 메서드이다.
itemPrice()
private long itemPrice(MenuItem menuItem) {
return cart.get(menuItem) * menuItem.getPrice();
}
showCart()에서 사용하는 상품의 주문수량 * 단가를 반환하는 메서드이다
ShoppingCart 클래스에서만 사용하기 때문에 private으로 선언하였다
Kiosk 클래스
package challenge.lv1;
import static challenge.util.PriceFormatter.priceFormat;
public class Kiosk {
private boolean flag;
private final ShoppingCart shoppingCart = new ShoppingCart();
// 다른 필드는 필수 LV5와 동일
// 메서드 따로 설명
}
주문 메뉴를 활성화 할 것인지 말 것인지를 판단하기 위한 flag를 선언했으며 false 이면 주문 메뉴는 활성화 되지 않는다
kiosk 에서 ShoppingCart 객체를 사용해야 하므로 생성했고 그 외의 필드는 도전과제 LV5와 같으므로 생략했다
전체 흐름을 관장하거나 흐름상에서 각종 처리해야하는 예외 로직들이 모두 Kiosk 클래스에 있다.
추가되거나 변경된 메서드들 위주로 설명하고 변경되지 않은 메서드드나 필드는 설명에서 제외했다.
start()
public void start() {
Scanner scanner = new Scanner(System.in);
int menusSize = menus.size(); // 등록한 메뉴의 개수를 변수화 (자주 사용함)
/**
* 프로그램 시작
*/
while (true) {
categoryPrinter(menus); // 메인 메뉴 -> 메뉴의 대분류(카테고리) 출력
// flag == true : 주문 로직 활성화
if (flag) {
orderMenuPrinter(); // 주문 메뉴 출력
}
int mainInput = inputValidator(scanner); // 입력값 검증 -> 메뉴 선택 or flag가 true일 때 주문 및 취소 선택
if (mainInput == 0) {
System.out.println("프로그램을 종료합니다.");
break;
}
/**
* 카테고리(Menu)가 고정개수가 아니라 여러 카테고리가 늘어날 수 있기 때문에 memusSize를 기준으로 로직을 작성
* flag == true: 주문 메뉴가 활성화되므로 menusSize + 2보다 크면 잘못된 입력값 간주(주문 메뉴는 2개로 고정임)
* flag == false: 주문 메뉴가 활성화 되지 않았으므로 menusSize보다 크면 잘못된 입력값으로 간주
*/
if (flag && mainInput > menusSize + 2|| !flag && mainInput > menusSize){
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
/**
* 주문 관련 로직
*/
if (mainInput == menusSize + 1) { // mainInput == 4 -> 주문 진행
String lastPriceFormat = order(shoppingCart); // 주문 내역 출력
int lastSelect = inputValidator(scanner); // 입력값 검증 -> 주문, 메뉴판 이동 선택
if (lastSelect == 1) { // lastSelect == 1 -> 최종 주문
System.out.println("주문이 완료 되었습니다. 금액은 ₩ " + lastPriceFormat + " 입니다");
shoppingCart.deleteAllCart(); // 주문이 완료되면 장바구니 초기화 후 flag = false 설정
flag = false;
continue;
} else if (lastSelect == 2) { // lastSelect == 2 -> 메뉴판 이동
System.out.println("메뉴로 돌아갑니다");
continue;
} else {
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
}
/**
* 주문 취소 로직 - 장바구니 초기화
*/
if (mainInput == menusSize + 2) {
System.out.println("장바구니가 비워졌습니다.");
System.out.println();
shoppingCart.deleteAllCart();
flag = false; // flag == false: 주문 메뉴 비활성화
continue;
}
/**
* 상세 메뉴 로직
*/
Menu menu = selectMainMenu(mainInput);
while (true) {
menuPrinter(menu); // 카테고리 하위 메뉴 출력
int menuInput = inputValidator(scanner); // 입력값 검증(재사용) -> 상세 메뉴 입력
if (menuInput == 0) {
System.out.println("처음으로 이동합니다.");
break;
}
if (menu.findAllMenuItem().size() < menuInput) {
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
MenuItem menuItem = selectMenuItem(menuInput, menu); // 선택된 메뉴를 출력하고 반환
/**
* 장바구니 관련 로직
*/
cartMenuPrinter(menuItem); // 장바구니 관련 메뉴 출력
int cartAddInput = inputValidator(scanner); // 입력값 검증 -> 장바구니 담기, 취소 선택
if (cartAddInput == 1) {
flag = true;
System.out.println("몇 개 주문 하시겠습니까? ");
int quantity = inputValidator(scanner); // 주문 수량
if (quantity == 0) {
System.out.println("장바구니에 담을 수 없습니다.");
flag = false;
continue;
}
shoppingCart.addCart(menuItem, quantity);
} else if (cartAddInput == 2) {
System.out.println("취소 하셨습니다. 이전으로 돌아갑니다.");
continue;
} else {
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
break;
}
}
}
전체 흐름을 관리하는 start()메서드의 변화가 가장 많다
이부분도 if - else 문 범벅이라 가독성이 좋지 않아 커맨드 별로 관리하고 싶었지만 일단 완성이 우선이라는 생각에 할 수 있는 예외 처리를 우선으로 하고 리펙토링은 LV2가 끝나고 시간이 남으면 할 예정이다
flag 값에 따라 주문 로직을 활성화 하는 로직이 들어있고, flag의 활성화 여부에 따라 처음 입력받는 값의 검증 로직이 동적으로 변경되도록 작성하였다
flag가 true가 되면 주문 로직이 활성화 되어 주문 로직에 접근할 수 있게 된다
상세 메뉴 로직에 접근하면 마지막에 선택한 아이템을 장바구니에 담을 수 있으며 장바구니에 담게 되면 자동으로 주문 로직이 활성화가 된다.
요구사항에는 없지만 장바구니에 담을 때 담을 아이템의 상품의 개수를 입력하는 기능도 추가했다.
주문 취소 버튼을 누르게 되면 장바구니의 내역이 모두 삭제되고 장바구니에 내역이 없으므로 flag를 false로 변환하여 주문 관련 로직이 나타나지 않도록 했다.
기본적으로 잘못된 메뉴를 선택하게 되면 해당 반복문이 다시 시작되도록 예외 로직을 처리하였고 각 메뉴마다 취소 버튼이나 처음으로 돌아가는 버튼도 동작하도록 작성되어있다.
최종적으로 주문을 확정하면 주문이 완료되며 최종 지불한 금액을 보여주고 다시 키오스크의 최초 메뉴로 돌아가며 이때 flag는 false로 장바구니는 초기화 시킨다.
처음에 도전 LV1을 구현할 때도 고민했던 부분인데, 장바구니를 적용할 때 수량을 어떤 클래스가 보관하면 좋을지 고민을 많이 했었다.
위에서 언급했듯 다뤄봤던 장바구니의 구조는 아이템 클래스에서 장바구니의 수량을 다루었는데 Map으로 장바구니를 구현하면 key로 바로 조회가 가능하니 장점도 있고 중복도 안되니 장점이 있을 것이라고 생각하여 Map<아이템, 수량> 구조로 장바구니의 구조로 LV1을 개발했고 LV2도 이 구조를 가지고 진행하고 있었다.
그러나 과제 요구사항에서 스트림을 사용해야하는 요구사항이 있어 구현에 어려움을 겪어 튜터링을 2번 받고 구조를 엎기로 결정했다.
일단 튜터링에서도 map으로 장바구니를 구현한 이유도 있고 개발한 것도 있으니 일단 이대로 구현하고 주석이나 설명으로 풀어도 된다고 하셨긴 했지만 MenuItem에서 수량을 관리하는게 맞다고 말씀 하셨기도 하고 Map보다는 List를 장바구니로 구현하는 것이 과제의 목적이기도 한 것 같아서 다시 개발했다.
아래는 다시 개발한 도전 LV2의 변경된 코드들에 대한 설명이다
DiscountPolicy - Enum
package challenge.lv2;
public enum DiscountPolicy {
PATRIOT(1, 10, "국가유공자"),
SOLDIER(2, 5, "군인"),
STUDENT(3, 3, "학생"),
BASIC(4, 0, "일반")
;
private final int input;
private final int discountRate;
private final String description;
DiscountPolicy(int input, int discountRate, String value) {
this.input = input;
this.discountRate = discountRate;
this.description = value;
}
public int getDiscountRate() {
return discountRate;
}
public int getInput() {
return input;
}
public String getDescription() {
return description;
}
/**
* totalPrice에 할인율을 적용하여 할인금액을 반환하는 메서드를 Enum에서 제공
*
* @param totalPrice 할인전 금액
* @return 할인 정책에 따른 할인 금액을 반환
*/
public long discount(long totalPrice) {
return totalPrice * discountRate / 100;
}
}
할인 정책을 관리하는 Enum 클래스이며 국가유공자(10%), 군인(5%), 학생(3%), 일반(할인 없음)으로 나뉘어져 있다
각 상수의 필드는 키오스크의 입력값이 숫자로 입력되므로 매칭 될 수 있는 input 값과, 할인율, 그리고 설명 필드가 있다
사실 ordinal()로 상수의 선언된 순서를 통해 Enum을 찾아도 되지만 이 방법은 실무에서 좋지 않다고 배웠다
예제에서는 할인정책이 변경되거나 할일은 없겠지만 실무에서는 할인 정책이 변경될 수도 선언된 순서도 바뀔 수 있기 때문에 버그가 발생할 가능성이 높아 가급적 다른 방법을 사용하는 것을 권장한다고 배웠다
그래서 여기서도 ordinal()을 사용하지않고 input필드와 입력값을 비교하여 할인 정책을 찾는 방법을 선택했다
Enum에서 할인율을 적용하여 할인 금액을 반환하는 메서드를 제공하도록 하여 할인 정책을 사용하는 곳에서는 할인 정책을 찾아서 discount()메서드만 호출하면 할인 금액을 알아내서 활용할 수 있다.
MenuItem 클래스
package challenge.lv2;
public class MenuItem {
// 기존 코드들은 그대로 유지 설명 생량
/**
* 장바구니에 담은 수량
*/
private int quantity;
public int getQuantity() {
return quantity;
}
public void addQuantity(int quantity) {
this.quantity += Math.abs(quantity);
}
public long itemPrice() {
return quantity * price;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
MenuItem menuItem = (MenuItem) o;
return getPrice() == menuItem.getPrice() && Objects.equals(getName(), menuItem.getName()) && Objects.equals(getDescription(), menuItem.getDescription());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getPrice(), getDescription());
}
}
변경된 설계로 MenuItem에서 수량을 관리하는 필드를 추가했으므로 수량을 얻을 수 있는 게터도 추가했다.
수량을 추가하기 위해 addQuantity(int quantity) 메서드를 추가하여 this.quantity 필드에 매개변수로 넘어온 수량을 더해지도록 했으며, 이때 실수로 음수가 작성되더라도 양수로 더해지도록 예외 처리 로직을 작성했다.
수량과 단가를 곱한 장바구니에 담긴 MenuItem의 금액을 출력하기 위한 itemPrice() 메서드도 제공한다
메서드 이름을 지을 때 너무 고민이 많았다..
총 금액도 아니고 합계 금액도 아니고 각 상품의 단가 * 수량 금액을 뭐라 표현할지 몰라서 단순하게 itemPrice()로 결정 지었다
이후 로직에서 객체를 동등성 비교를 할일이 많이 있어서 equals()와 hashCode()를 오버라이딩 해두었다.
ShoppingCart 클래스
package challenge.lv2;
public class ShoppingCart {
private final List<MenuItem> cart = new ArrayList<>();
// 메서드들 별도로 설명
}
변경된 구조로 코드를 개선하기 위해 장바구니를 List로 선언하고 구현체를 ArrayList를 선택했다
중복이 되지 않는 Set 컬렉션으로 하려고 했으나 순서가 보장이 안되므로 List 자료구조를 선택했다.
addCart()
public void addCart(MenuItem menuItem, int quantity) {
boolean matchResult = cart.stream().anyMatch(cartItem -> cartItem.equals(menuItem));
if (matchResult) {
menuItem.addQuantity(quantity);
} else {
menuItem.addQuantity(quantity);
cart.add(menuItem);
}
System.out.println(menuItem.getName() + " " + quantity + " 개가 장바구니에 추가 되었습니다.");
}
장바구니에 아이템을 저장하고 수량을 MenuItem 필드에 추가하는 메서드이다.
Stream을 활용하여 저장할 MenuItem이 기존 장바구니에 있는지 확인하여 있다면 MenuItem에 수량만 추가하고, 없다면 MenuItem에 수량을 저장하고 해당 MenuItem을 장바구니에 담는다
MenuItem에서 이미 equals()와 hashCode()를 오버라이딩 해두었으므로 해당 필드가 아니라 객체 자체를 equals()비교 해도 동등성 비교가 적용된다.
이후 저장한 상품과 수량을 출력해준다.
showCart(), getTotalPrice()
public void showCart() {
System.out.println("[Orders]");
cart.stream().forEach(menuItem -> System.out.printf("%-18s | %d개 | 상품 합계: ₩ %10s | %s%n",
// itemPrice(): 수량이 반영된 개별 메뉴당 합계 가격
menuItem.getName(), menuItem.getQuantity(), priceFormat(menuItem.itemPrice()) , menuItem.getDescription()));
System.out.println();
}
public long getTotalPrice() {
long totalPrice = cart.stream()
.mapToLong(MenuItem::itemPrice)
.sum();
System.out.println("[Total]");
String priceFormat = priceFormat(totalPrice);
System.out.println("₩ " + priceFormat);
return totalPrice;
}
기존에는 showCart()에서 장바구니의 내역과 전체 금액을 보여주는 로직이 분리 되지 않고 한 번에 작성되어 있어서 마음에 안들었는데 구조를 개편하고 요구사항을 충족하기 위해서는 분리가 필요해보여서 분리했다.
showCart()는 단순히 장바구니의 내역을 Stream을 활용하여 반복해서 값을 꺼내 출력해주고, 이 때 가격은 포맷팅해서 보여준다
getTotalPrice()도 마찬가지로 스트림을 통해 장바구니의 MenuItem들을 순회하여 itemPrice() 메서드를 호출하여 각 MenuItem의 단가 * 수량의 가격을 꺼내서 sum() 최종 연산자로 반환한다
이 때 단일 함수를 사용하고 메서드 참조로도 가독성이 나쁘지 않아서 람다식을 메서드 참조로 활용했다.
람다식으로 표현한다면 (menuItem -> menuItem.itemPrice())로 작성했을 것이다.
반환된 장바구니의 최종 합계 금액을 포맷팅하여 화면에 출력하고, 메서드의 반환값으로는 포맷팅 하기전의 long 타입의 최종 금액을 반환하는데, 이 값을 가지고 이후 할인 정책에 따라 할인을 적용할 것이다.
getDiscountPrice()
public String getDiscountPrice(long totalPrice, int discountNumber) {
DiscountPolicy[] values = DiscountPolicy.values();
DiscountPolicy discountPolicy = Arrays.stream(values)
.filter(policy -> policy.getInput() == discountNumber)
.findFirst().get();
long discount = discountPolicy.discount(totalPrice);
return priceFormat(totalPrice - discount);
}
DiscountPolicy.values()를 활용하여 Enum을 배열로 변환한 후 Arrays.stream()을 활용하여 배열을 스트림으로 변환하여 filter 조건에 맞는 할인 정책을 반환하도록 했다.
원래 자주 사용하던 코드라면 향상된 반복문이나 forEach()를 사용했을테지만 도전 LV2는 가급적 Stream을 사용하는 것을 권장하는 같아서 Stream을 사용했다
여기서 Optional()로 반환되는 객체를 get()으로 단순히 반환했는데 할인 정책을 찾지 못해서 예외가 터질 가능성이 없으므로 이렇게 코드를 작성했다.
반환된 할인 정책의 discount(totalPrice)를 호출하여 정책에 적용된 할인 금액을 반환하고 최종적으로 장바구니의 전체 금액에 할인금액을 빼서 포맷팅하여 반환한다.
deleteOneCartList()
public void deleteOneCartList(String cancelMenuItemName) {
int index = IntStream.range(0, cart.size())
.filter(i -> cart.get(i).getName().equalsIgnoreCase(cancelMenuItemName))
.findFirst()
.getAsInt();
System.out.println(cart.get(index).getName() + " 가 장바구니에서 제거 되었습니다.");
cart.remove(index);
}
장바구니의 특정 메뉴를 제거하는 메서드이다.
IntStream.range(0, cart.size())로 스트림의 반복을 0부터 cart.size() -1까지 순회하도록 하고 그 이후 .filter를 통해 cart.get(i)로 조회한 장바구니의 MenuItem의 상품명과 매개변수로 삭제할 상품명을 대소문자 구분 없이 비교하도록 했다.
사용자의 입력값을 대소문자 구분 없이 검증한 이유는 사용자의 편의를 위함이다..
사실 사용자의 입력값을 int로 받아서 입력받은 int값을 index로 활용하여 바로 장바구니의 데이터를 O(1)의 시간복잡도로 지울 수 있지만 스트림을 활용해야한다는 요구사항때문에 매개변수로 상품명을 입력받도록 구성했다.
filter 하나라도 검증이 완료되면 getAsInt()로 스트림의 반복 횟수값을 반환받아서 이것을 index로 활용하여 장바구니의 상품을 remove(index)를 사용하여 O(1)의 삭제 성능을 보이도록 작성했다.
Kiosk 클래스
필드의 구조는 동일하므로 생략하고 변경되는 메서드만 작성했다
start()
public void start() {
Scanner scanner = new Scanner(System.in);
int menusSize = menus.size(); // 등록한 메뉴의 개수를 변수화 (자주 사용함)
while (true) {
categoryPrinter(menus); // 메인 메뉴 -> 메뉴의 대분류(카테고리) 출력
// flag == true : 주문 로직 활성화
if (flag) {
orderMenuPrinter(); // 주문 메뉴 출력
}
int mainInput = inputValidator(scanner); // 입력값 검증 -> 메뉴 선택 or flag가 true일 때 주문 및 취소 선택
if (mainInput == 0) {
System.out.println("프로그램을 종료합니다.");
break;
}
if (flag && mainInput > menusSize + 2|| !flag && mainInput > menusSize){
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
if (mainInput == menusSize + 1) { // mainInput == 4 -> 주문 진행
long totalPrice = checkOrder(shoppingCart); // 주문 내역 확인 및 전체 금액 반환
int orderInput = inputValidator(scanner); // 입력값 검증 -> 주문, 메뉴판 이동 선택
if (orderInput == 1) { // lastSelect == 1 -> 최종 주문
discountWithOrder(scanner, totalPrice); // 할인 적용 및 최종 주문 로직
continue;
} else if (orderInput == 2) { // lastSelect == 2 -> 메뉴판 이동
System.out.println("메뉴로 돌아갑니다");
continue;
} else {
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
}
if (mainInput == menusSize + 2) { // mainInput == 5 -> 주문 취소 진행
System.out.println("1. 전체 취소\t 2. 부분 취소");
int cancelInput = inputValidator(scanner); // 입력값 검증(재사용) -> 취소 메뉴
if (cancelInput == 1) { // 전체 취소
allCancel();
continue;
} else if (cancelInput == 2) { // 부분 취소
partCancel(scanner); // 대,소문자 구분하지 않음
continue;
}
}
Menu menu = selectMainMenu(mainInput);
while (true) {
menuPrinter(menu); // 카테고리 하위 메뉴 출력
int menuInput = inputValidator(scanner); // 입력값 검증(재사용) -> 상세 메뉴 입력
if (menuInput == 0) {
System.out.println("처음으로 이동합니다.");
break;
}
if (menu.findAllMenuItem().size() < menuInput) {
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
MenuItem menuItem = selectMenuItem(menuInput, menu); // 선택된 메뉴를 출력하고 반환
cartMenuPrinter(menuItem); // 장바구니 관련 메뉴 출력
int cartAddInput = inputValidator(scanner); // 입력값 검증 -> 장바구니 담기, 취소 선택
if (cartAddInput == 1) {
flag = true;
System.out.println("몇 개 주문 하시겠습니까? ");
int quantity = inputValidator(scanner); // 주문 수량
if (quantity == 0) {
System.out.println("장바구니에 담을 수 없습니다.");
flag = false;
continue;
}
shoppingCart.addCart(menuItem, quantity);
} else if (cartAddInput == 2) {
System.out.println("취소 하셨습니다. 이전으로 돌아갑니다.");
continue;
} else {
System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
continue;
}
break;
}
}
}
전제적인 실행 흐름은 도전 LV1과 동일한데 일부 기능을 메서드화 시켰고, 장바구니 취소 관련 로직과 할인 정책을 적용한 주문 관련 로직이 추가 되었다
장바구니에 물건을 담은 후 활성화된 주문 로직으로 진입하면 checkOrder()메서드를 통해 주문 내역과 전체금액을 먼저 반환하고 그 이후 주문을 하게 되면 disWithCountOrder()메서드를 통해 할인 정책을 선택하고 최종 주문이 완료 된다.
주문을 취소하는 로직도 특정 장바구니의 상품을 제외하는 로직이 추가 되었는데 해당 로직을 allCancel(), partCencel()로 메서드화 하며 메서드 이름으로 구분지을 수 있도록 그나마 가독성을 챙겨주었다.
checkOrder(), discountWithOrder(), discountMenuPrinter() - 주문 관련 로직 메서드
private long checkOrder(ShoppingCart shoppingCart) {
System.out.println("아래와 같이 주문 하시겠습니까?");
System.out.println();
shoppingCart.showCart();
long totalPrice = shoppingCart.getTotalPrice();
System.out.println();
System.out.println("1. 확인\t 2. 메뉴판");
return totalPrice;
}
private void discountWithOrder(Scanner scanner, long totalPrice) {
discountMenuPrinter();
int discountInput = inputValidator(scanner); // 입력값 검증 -> 할인 정책 적용
if (discountInput == 0) {
System.out.println("잘못된 할인 정책을 입력하셨습니다. 다시 주문을 확인해 주세요");
return;
}
String discountPriceFormat = shoppingCart.getDiscountPrice(totalPrice, discountInput);
System.out.println("주문이 완료 되었습니다. 금액은 ₩ " + discountPriceFormat + " 입니다");
shoppingCart.deleteAllCart(); // 주문이 완료되면 장바구니 초기화
flag = false; // flag = false 설정
}
private void discountMenuPrinter() {
System.out.println("할인 메뉴를 선택해 주세요.");
Arrays.stream(DiscountPolicy.values())
.forEach(policy ->
System.out.printf("%d. %-6s : %2d%%%n", policy.getInput(), policy.getDescription(), policy.getDiscountRate()));
}
checkOrder()
기존에 order() 메서드라는 이름으로 존재했던 메서드이며 기능은 동일하다
차이점은 반환값을 포맷팅된 장바구니의 총 금액이 아니라 long 타입으로 반환하여 이후에 해당 값을 받아서 수정할 수 있다
discountWithOrder()
주문이 확정되면 discountMenuPrinter()를 호출하여 할인 메뉴를 보여주고 사용자가 할인 정책을 선택하면 shoppingCart.getDiscountPrice(totalPrice, discountInput)을 장바구니의 총 금액과 사용자가 선택한 할인 정책 번호를 인자로 넘겨주면서 호출한다
반환값으로 할인 금액이 적용된 최종 주문 금액을 받아서 출력해주고 장바구니 초기화 및 flag = false로 설정하여 키오스크의 초기 메뉴로 돌아간다.
discountMenuPrinter()
사용자가 할인 정책을 선택할 수 있도록 할인 메뉴를 출력해주며, 나름 포맷을 맞춰보기 위해 printf문을 사용했다.
Stream연습을 위해 강제로 Stream()을 사용해 보았으며 일반적로는 향상된 for문을사용했을 것 같다.
allCancel(), partCancel() - 주문 취소(장바구니 목록 제거) 관련 메서드들
private void allCancel() {
System.out.println("장바구니가 비워졌습니다.");
System.out.println();
shoppingCart.deleteAllCart();
flag = false; // flag == false: 주문 메뉴 비활성화
}
private void partCancel(Scanner scanner) {
System.out.println("장바구니에서 삭제할 상품 이름을 입력해 주세요.");
shoppingCart.showCart();
System.out.print("삭제할 상품명:");
String cancelMenuInput = scanner.nextLine();
shoppingCart.deleteOneCartList(cancelMenuInput);
}
allCancel()
도전 LV1에서 start()로직에 존재했던 주문 취소 로직을 부분 취소가 생김으로 인해 메서드화 했다.
로직은 동일하며 shoopingCart.deleteAllCart()를 호출하여 장바구니를 초기화 시키고 flag = false로 세팅하며 처음으로 돌아간다
partCancel()
장바구니의 특정 메뉴를 삭제하는 메서드로 사용자가 장바구니에서 삭제할 대상을 문자열로 입력하면 입력된 값을 shoppingCart.deleteOneCartList(cancelMenuInput)으로 인자로 넘기면서 호출한다.
이때 deleteOneCartList()메서드 내부에서는 사용자 편의를 위해 대소문자를 구분하지 않는다.
요구사항을 충족해보기 위해 deleteOneCartList()에서 강제로 스트림을 사용하도록 구현하여 문자열을 입력 받았지만 원래 구조대로하면 숫자로 입력 받아서 이 값을 index로 활용하여 장바구니의 리스트를 삭제하도록 구현할 것 같다.