diff --git a/build.gradle b/build.gradle index cf1e54f32..f12f02cdc 100644 --- a/build.gradle +++ b/build.gradle @@ -15,9 +15,8 @@ dependencies { } java { - toolchain { - languageVersion = JavaLanguageVersion.of(11) - } + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } test { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 28ff446a2..a59520664 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/menu/Application.java b/src/main/java/menu/Application.java index 6340b6f33..95188617d 100644 --- a/src/main/java/menu/Application.java +++ b/src/main/java/menu/Application.java @@ -1,7 +1,13 @@ package menu; +import menu.view.InputView; +import menu.view.OutputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + MenuRecommendation menuRecommendation = new MenuRecommendation(inputView, outputView); + menuRecommendation.run(); } } diff --git a/src/main/java/menu/MenuRecommendation.java b/src/main/java/menu/MenuRecommendation.java new file mode 100644 index 000000000..026f8fdf2 --- /dev/null +++ b/src/main/java/menu/MenuRecommendation.java @@ -0,0 +1,72 @@ +package menu; + +import java.util.List; +import java.util.function.Supplier; +import menu.domain.FoodCategory; +import menu.domain.ImpossibilityMenu; +import menu.domain.ImpossibilityMenus; +import menu.domain.MenuRecommender; +import menu.domain.Person; +import menu.domain.Persons; +import menu.domain.RecommendMenus; +import menu.domain.RecommendationFoodCategories; +import menu.view.InputView; +import menu.view.OutputView; + +public class MenuRecommendation { + + private final InputView inputView; + private final OutputView outputView; + + public MenuRecommendation(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + outputView.printStartApplication(); + Persons persons = retryOnError(this::getPersons); + MenuRecommender menuRecommender = getMenuRecommender(persons); + + RecommendationFoodCategories recommendationFoodCategories = new RecommendationFoodCategories(); + List categories = recommendationFoodCategories.getRecommendationFoodCategories(); + + RecommendMenus recommendMenus = new RecommendMenus(persons); + for (FoodCategory category : categories) { + for (Person person : persons.getPersons()) { + String menu = menuRecommender.recommendMenu(person, category, recommendMenus); + recommendMenus.addMenu(person, menu); + } + } + outputView.printRecommendationResult(categories, recommendMenus.getRecommendMenus()); + } + + private T retryOnError(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + private Persons getPersons() { + List personNames = inputView.readPersonNames(); + return new Persons(personNames); + } + + private MenuRecommender getMenuRecommender(Persons persons) { + ImpossibilityMenus impossibilityMenus = new ImpossibilityMenus(); + for (Person person : persons.getPersons()) { + ImpossibilityMenu menu = retryOnError(() -> getImpossibilityMenu(person)); + impossibilityMenus.addImpossibilityMenu(person, menu); + } + return new MenuRecommender(impossibilityMenus); + } + + private ImpossibilityMenu getImpossibilityMenu(Person person) { + List menus = inputView.readImpossibilityMenuName(person.getName()); + return new ImpossibilityMenu(menus); + } +} diff --git a/src/main/java/menu/domain/FoodCategory.java b/src/main/java/menu/domain/FoodCategory.java new file mode 100644 index 000000000..7d35e90d4 --- /dev/null +++ b/src/main/java/menu/domain/FoodCategory.java @@ -0,0 +1,49 @@ +package menu.domain; + +import java.util.Arrays; +import menu.exception.ErrorCode; + +public enum FoodCategory { + + JAPANESE(1, "일식"), + KOREAN(2, "한식"), + CHINESE(3, "중식"), + ASIAN(4, "아시안"), + WESTERN(5, "양식"), + ; + + private final int number; + private final String name; + + FoodCategory(int number, String name) { + this.number = number; + this.name = name; + } + + public static FoodCategory from(int number) { + return Arrays.stream(values()) + .filter(category -> category.number == number) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ErrorCode.INVALID_NUMBER.getMessage())); + } + + public static int getFirstNumber() { + return Arrays.stream(values()) + .map(category -> category.number) + .sorted() + .toList() + .get(0); + } + + public static int getLastNumber() { + return Arrays.stream(values()) + .map(category -> category.number) + .sorted((o1, o2) -> o2 - o1) + .toList() + .get(0); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/menu/domain/ImpossibilityMenu.java b/src/main/java/menu/domain/ImpossibilityMenu.java new file mode 100644 index 000000000..583bcb8aa --- /dev/null +++ b/src/main/java/menu/domain/ImpossibilityMenu.java @@ -0,0 +1,27 @@ +package menu.domain; + +import java.util.List; +import menu.exception.ErrorCode; + +public class ImpossibilityMenu { + + public static final int MAX_IMPOSSIBILITY_MENUS_COUNT = 2; + + private final List impossibilityMenus; + + public ImpossibilityMenu(List impossibilityMenus) { + validate(impossibilityMenus); + impossibilityMenus.forEach(MenuBoard::validateMenuName); + this.impossibilityMenus = impossibilityMenus; + } + + private void validate(List impossibilityMenus) { + if (impossibilityMenus.size() > MAX_IMPOSSIBILITY_MENUS_COUNT) { + throw new IllegalArgumentException(ErrorCode.INVALID_MAX_IMPOSSIBILITY_MENUS_COUNT.getMessage()); + } + } + + public boolean contains(String menu) { + return impossibilityMenus.contains(menu); + } +} diff --git a/src/main/java/menu/domain/ImpossibilityMenus.java b/src/main/java/menu/domain/ImpossibilityMenus.java new file mode 100644 index 000000000..ef27ba00f --- /dev/null +++ b/src/main/java/menu/domain/ImpossibilityMenus.java @@ -0,0 +1,21 @@ +package menu.domain; + +import java.util.HashMap; +import java.util.Map; + +public class ImpossibilityMenus { + + private final Map impossibilityMenus; + + public ImpossibilityMenus() { + this.impossibilityMenus = new HashMap<>(); + } + + public void addImpossibilityMenu(Person person, ImpossibilityMenu menus) { + impossibilityMenus.put(person, menus); + } + + public ImpossibilityMenu getImpossibilityMenu(Person person) { + return impossibilityMenus.get(person); + } +} diff --git a/src/main/java/menu/domain/MenuBoard.java b/src/main/java/menu/domain/MenuBoard.java new file mode 100644 index 000000000..3b3ee1d46 --- /dev/null +++ b/src/main/java/menu/domain/MenuBoard.java @@ -0,0 +1,39 @@ +package menu.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.List; +import java.util.Map; +import menu.exception.ErrorCode; + +public class MenuBoard { + + private static final Map> menuBoard = Map.of( + FoodCategory.JAPANESE, List.of("규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼"), + FoodCategory.KOREAN, List.of("김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음"), + FoodCategory.CHINESE, List.of("깐풍기", "볶음면", "동파육", "짜장면", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채"), + FoodCategory.ASIAN, List.of("팟타이", "카오 팟", "나시고렝", "파인애플 볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜"), + FoodCategory.WESTERN, List.of("라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니") + ); + + private MenuBoard() { + } + + public static void validateMenuName(String menuName) { + List isContains = menuBoard.values().stream() + .map(menus -> menus.contains(menuName)) + .toList(); + if (!isContains.contains(true)) { + throw new IllegalArgumentException(ErrorCode.INVALID_MENU_NAME.getMessage()); + } + } + + public static String getMenusExcluding(FoodCategory category, ImpossibilityMenu impossibilityMenus) { + List menus = menuBoard.get(category); + while (true) { + String menu = Randoms.shuffle(menus).get(0); + if (!impossibilityMenus.contains(menu)) { + return menu; + } + } + } +} diff --git a/src/main/java/menu/domain/MenuRecommender.java b/src/main/java/menu/domain/MenuRecommender.java new file mode 100644 index 000000000..708c07989 --- /dev/null +++ b/src/main/java/menu/domain/MenuRecommender.java @@ -0,0 +1,21 @@ +package menu.domain; + +public class MenuRecommender { + + private final ImpossibilityMenus impossibilityMenus; + + public MenuRecommender(ImpossibilityMenus impossibilityMenus) { + this.impossibilityMenus = impossibilityMenus; + } + + public String recommendMenu(Person person, FoodCategory category, RecommendMenus recommendMenus) { + ImpossibilityMenu impossibilityMenu = impossibilityMenus.getImpossibilityMenu(person); + String menu = null; + boolean recommended = true; + while (recommended) { + menu = MenuBoard.getMenusExcluding(category, impossibilityMenu); + recommended = recommendMenus.containsMenu(person, menu); + } + return menu; + } +} diff --git a/src/main/java/menu/domain/Person.java b/src/main/java/menu/domain/Person.java new file mode 100644 index 000000000..9967fe497 --- /dev/null +++ b/src/main/java/menu/domain/Person.java @@ -0,0 +1,29 @@ +package menu.domain; + +import menu.exception.ErrorCode; + +public class Person { + + public static final int MIN_NAME_LENGTH = 2; + public static final int MAX_NAME_LENGTH = 4; + + private final String name; + + public Person(String name) { + validate(name); + this.name = name; + } + + private void validate(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException(ErrorCode.INVALID_NAME.getMessage()); + } + if (name.length() < MIN_NAME_LENGTH || name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException(ErrorCode.INVALID_NAME_LENGTH.getMessage()); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/menu/domain/Persons.java b/src/main/java/menu/domain/Persons.java new file mode 100644 index 000000000..54fcf9f82 --- /dev/null +++ b/src/main/java/menu/domain/Persons.java @@ -0,0 +1,32 @@ +package menu.domain; + +import java.util.List; +import menu.exception.ErrorCode; + +public class Persons { + + public static final int MIN_PERSONS_COUNT = 2; + public static final int MAX_PERSONS_COUNT = 5; + + private final List persons; + + public Persons(List names) { + validate(names); + this.persons = names.stream() + .map(Person::new) + .toList(); + } + + private void validate(List names) { + if (names.size() < MIN_PERSONS_COUNT) { + throw new IllegalArgumentException(ErrorCode.INVALID_MIN_PERSONS_COUNT.getMessage()); + } + if (names.size() > MAX_PERSONS_COUNT) { + throw new IllegalArgumentException(ErrorCode.INVALID_MAX_PERSONS_COUNT.getMessage()); + } + } + + public List getPersons() { + return persons; + } +} diff --git a/src/main/java/menu/domain/RecommendMenus.java b/src/main/java/menu/domain/RecommendMenus.java new file mode 100644 index 000000000..973782a6f --- /dev/null +++ b/src/main/java/menu/domain/RecommendMenus.java @@ -0,0 +1,30 @@ +package menu.domain; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RecommendMenus { + + private final Map> recommendMenus; + + public RecommendMenus(Persons persons) { + this.recommendMenus = new LinkedHashMap<>(); + for (Person person : persons.getPersons()) { + recommendMenus.put(person, new ArrayList<>()); + } + } + + public boolean containsMenu(Person person, String menu) { + return recommendMenus.get(person).contains(menu); + } + + public void addMenu(Person person, String menu) { + recommendMenus.get(person).add(menu); + } + + public Map> getRecommendMenus() { + return recommendMenus; + } +} diff --git a/src/main/java/menu/domain/RecommendationFoodCategories.java b/src/main/java/menu/domain/RecommendationFoodCategories.java new file mode 100644 index 000000000..b8d4710f5 --- /dev/null +++ b/src/main/java/menu/domain/RecommendationFoodCategories.java @@ -0,0 +1,33 @@ +package menu.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.ArrayList; +import java.util.List; + +public class RecommendationFoodCategories { + + private static final int MAX_CATEGORY_COUNT = 5; + private static final int MAX_CATEGORY_DUPLICATE = 2; + + private final List recommendationFoodCategories; + + public RecommendationFoodCategories() { + List categories = new ArrayList<>(); + while (categories.size() < MAX_CATEGORY_COUNT) { + FoodCategory category = FoodCategory.from( + Randoms.pickNumberInRange(FoodCategory.getFirstNumber(), FoodCategory.getLastNumber())); + List foodCategories = categories.stream() + .filter(existingCategory -> existingCategory == category) + .toList(); + if (foodCategories.size() < MAX_CATEGORY_DUPLICATE) { + categories.add(category); + } + } + + this.recommendationFoodCategories = categories; + } + + public List getRecommendationFoodCategories() { + return recommendationFoodCategories; + } +} diff --git a/src/main/java/menu/exception/ErrorCode.java b/src/main/java/menu/exception/ErrorCode.java new file mode 100644 index 000000000..601850ded --- /dev/null +++ b/src/main/java/menu/exception/ErrorCode.java @@ -0,0 +1,36 @@ +package menu.exception; + +import menu.domain.ImpossibilityMenu; +import menu.domain.Person; +import menu.domain.Persons; + +public enum ErrorCode { + + INVALID_NAME("이름은 빈 칸일 수 없습니다"), + INVALID_NAME_LENGTH( + String.format("이름은 %d자 이상 %d자 이하이어야 합니다", Person.MIN_NAME_LENGTH, Person.MAX_NAME_LENGTH) + ), + INVALID_MIN_PERSONS_COUNT( + String.format("코치는 최소 %d명 이상 입력해야 합니다.", Persons.MIN_PERSONS_COUNT) + ), + INVALID_MAX_PERSONS_COUNT( + String.format("코치는 최대 %d명 이하로 입력해야 합니다.", Persons.MAX_PERSONS_COUNT) + ), + INVALID_MENU_NAME("존재하지 않는 메뉴 이름입니다."), + INVALID_MAX_IMPOSSIBILITY_MENUS_COUNT( + String.format("불가능한 메뉴는 최대 2개까지 지정할 수 있습니다.", ImpossibilityMenu.MAX_IMPOSSIBILITY_MENUS_COUNT) + ), + INVALID_NUMBER("해당 번호의 카테고리는 없습니다."); + + private static final String prefix = "[ERROR] "; + + private final String message; + + ErrorCode(String message) { + this.message = message; + } + + public String getMessage() { + return prefix + message; + } +} diff --git a/src/main/java/menu/view/InputView.java b/src/main/java/menu/view/InputView.java new file mode 100644 index 000000000..1bef08c30 --- /dev/null +++ b/src/main/java/menu/view/InputView.java @@ -0,0 +1,26 @@ +package menu.view; + +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private final Scanner scanner = new Scanner(System.in); + + public List readPersonNames() { + System.out.println("코치의 이름을 입력해 주세요. (, 로 구분)"); + List names = List.of(scanner.nextLine() + .split(",")); + System.out.println(); + return names; + } + + public List readImpossibilityMenuName(String name) { + String format = String.format("%s(이)가 못 먹는 메뉴를 입력해 주세요.", name); + System.out.println(format); + List menus = List.of(scanner.nextLine() + .split(",")); + System.out.println(); + return menus; + } +} diff --git a/src/main/java/menu/view/OutputView.java b/src/main/java/menu/view/OutputView.java new file mode 100644 index 000000000..5f4f4236e --- /dev/null +++ b/src/main/java/menu/view/OutputView.java @@ -0,0 +1,35 @@ +package menu.view; + +import java.util.List; +import java.util.Map; +import menu.domain.FoodCategory; +import menu.domain.Person; + +public class OutputView { + + public void printStartApplication() { + System.out.println("점심 메뉴 추천을 시작합니다."); + System.out.println(); + } + + public void printRecommendationResult(List categories, Map> menus) { + System.out.println("메뉴 추천 결과입니다."); + System.out.println("[ 구분 | 월요일 | 화요일 | 수요일 | 목요일 | 금요일 ]"); + + String categoryLine = String.join(" | ", + categories.stream().map(FoodCategory::getName).toList()); + System.out.println("[ 카테고리 | " + categoryLine + " ]"); + + for (Person person : menus.keySet()) { + String menuLine = String.join(" | ", menus.get(person)); + System.out.println("[ " + person.getName() + " | " + menuLine + " ]"); + } + + System.out.println(); + System.out.println("추천을 완료했습니다."); + } + + public void printErrorMessage(String message) { + System.out.println(message); + } +}