diff --git a/docs/README.md b/docs/README.md index e69de29bb..0bb12129d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,27 @@ +### 구현 기능 목록 + +**출력** +- [x] 실행 문구 +- [x] 이름 입력 안내 +- [x] 못 먹는 음식 입력 안내 +- [x] 추천 요일 +- [x] 추천 카테고리 +- [x] 메뉴 추천 결과 +- [x] 추천 완료 문구 + +**입력** +- [x] 코치 이름 +- [x] 못 먹는 음식 + +**카테고리 추천** +- [x] 랜덤으로 카테고리를 뽑는다. +- [x] 3번 이상 먹은 카테고리일 경우 다시 뽑는다. + +**음식 추천** +- [x] 카테고리의 음식 중 하나를 뽑는다. +- [x] 코치가 못 먹는 음식일 경우 다시 뽑는다. +- [x] 이미 먹은 음식일 경우 다시 뽑는다. + +**오류 발생** +- [x] 코치의 이름이 2~4글자 이상인 경우 +- [x] `,`로 나누어져 있지 않은 경우 \ No newline at end of file diff --git a/src/main/java/menu/Application.java b/src/main/java/menu/Application.java index 6340b6f33..6b04e2e23 100644 --- a/src/main/java/menu/Application.java +++ b/src/main/java/menu/Application.java @@ -1,7 +1,10 @@ package menu; +import menu.controller.MenuController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + MenuController menuController = new MenuController(); + menuController.start(); } } diff --git a/src/main/java/menu/constant/Category.java b/src/main/java/menu/constant/Category.java new file mode 100644 index 000000000..f01ad30a3 --- /dev/null +++ b/src/main/java/menu/constant/Category.java @@ -0,0 +1,32 @@ +package menu.constant; + +import java.util.Arrays; + +public enum Category { + + JAPANESE(1, "일식"), + KOREAN(2, "한식"), + CHINESE(3, "중식"), + ASIAN(4, "아시안"), + WESTERN(5, "양식"); + + private final int index; + private final String type; + + Category(int index, String type) { + this.index = index; + this.type = type; + } + + public static Category getRecommendFood(int randomNumber) { + return Arrays.stream(Category.values()) + .filter(category -> category.index == randomNumber) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(ExceptionMessage.INCORRECT_RECOMMEND.toString())); + } + + @Override + public String toString() { + return type; + } +} diff --git a/src/main/java/menu/constant/Days.java b/src/main/java/menu/constant/Days.java new file mode 100644 index 000000000..c6ce7ae5e --- /dev/null +++ b/src/main/java/menu/constant/Days.java @@ -0,0 +1,21 @@ +package menu.constant; + +public enum Days { + + MONDAY("월요일"), + TUESDAY("화요일"), + WEDNESDAY("수요일"), + THURSDAY("목요일"), + FRIDAY("금요일"); + + private final String day; + + Days(String day) { + this.day = day; + } + + @Override + public String toString() { + return day; + } +} diff --git a/src/main/java/menu/constant/ExceptionMessage.java b/src/main/java/menu/constant/ExceptionMessage.java new file mode 100644 index 000000000..0b8923040 --- /dev/null +++ b/src/main/java/menu/constant/ExceptionMessage.java @@ -0,0 +1,21 @@ +package menu.constant; + +public enum ExceptionMessage { + + INCORRECT_RECOMMEND("잘못된 추천입니다."), + INCORRECT_CATEGORY("존재하지 않는 카테고리입니다."), + INCORRECT_NAME_RANGE("코치의 이름은 2~4자까지 입력할 수 있습니다."), + INCORRECT_DELIMITER("','로 구분해 입력하여 주십시오."); + + private static final String PREFIX = "[ERROR] "; + private final String message; + + ExceptionMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return PREFIX + message; + } +} \ No newline at end of file diff --git a/src/main/java/menu/constant/Menu.java b/src/main/java/menu/constant/Menu.java new file mode 100644 index 000000000..03e662955 --- /dev/null +++ b/src/main/java/menu/constant/Menu.java @@ -0,0 +1,27 @@ +package menu.constant; + +import java.util.Arrays; +import java.util.List; + +public enum Menu { + JAPANESE(Category.JAPANESE, List.of("규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼")), + KOREAN(Category.KOREAN, List.of("김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음")), + CHINESE(Category.CHINESE, List.of("깐풍기", "볶음면", "동파육", "짜장면", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채")), + ASIAN(Category.ASIAN, List.of("팟타이", "카오 팟", "나시고렝", "파인애플", "볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜")), + WESTERN(Category.WESTERN, List.of("라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니")); + + private final Category category; + private final List foods; + Menu(Category category, List foods) { + this.category = category; + this.foods = foods; + } + + public static List getFoodsByCategory(Category category) { + return Arrays.stream(Menu.values()) + .filter(value -> value.category == category) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(ExceptionMessage.INCORRECT_CATEGORY.toString())) + .foods; + } +} diff --git a/src/main/java/menu/constant/OutputMessage.java b/src/main/java/menu/constant/OutputMessage.java new file mode 100644 index 000000000..4ac516948 --- /dev/null +++ b/src/main/java/menu/constant/OutputMessage.java @@ -0,0 +1,22 @@ +package menu.constant; + +public enum OutputMessage { + + START_MESSAGE("점심 메뉴 추천을 시작합니다."), + COACH_NAME("코치의 이름을 입력해 주세요. (, 로 구분)"), + EXCLUDED_FOOD("%s(이)가 못 먹는 메뉴를 입력해 주세요.\n"), + DAYS_TITLE("구분"), + MENU_RESULT("메뉴 추천 결과입니다."), + COMPLETE("추천을 완료했습니다."); + + private final String message; + + OutputMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return this.message; + } +} \ No newline at end of file diff --git a/src/main/java/menu/constant/ResultElement.java b/src/main/java/menu/constant/ResultElement.java new file mode 100644 index 000000000..a91db3031 --- /dev/null +++ b/src/main/java/menu/constant/ResultElement.java @@ -0,0 +1,19 @@ +package menu.constant; + +public enum ResultElement { + START("[ "), + END(" ]"), + DELIMITER(" | "); + + + private final String element; + + ResultElement(String element) { + this.element = element; + } + + @Override + public String toString() { + return this.element; + } +} diff --git a/src/main/java/menu/controller/MenuController.java b/src/main/java/menu/controller/MenuController.java new file mode 100644 index 000000000..debe7aa51 --- /dev/null +++ b/src/main/java/menu/controller/MenuController.java @@ -0,0 +1,31 @@ +package menu.controller; + +import menu.domain.Name; +import menu.service.MenuService; +import menu.view.InputView; +import menu.view.OutputView; + +import java.util.List; + +public class MenuController { + + private final OutputView outputView = new OutputView(); + private final InputView inputView = new InputView(); + private final MenuService menuService = new MenuService(); + + public void start() { + outputView.printStartMessage(); + addCoachAndExcludedFoods(); + menuService.recommendMenus(); + outputView.printResult(menuService.getCategories(), menuService.getCoaches()); + } + + private void addCoachAndExcludedFoods() { + List coaches = inputView.readNames(); + coaches.forEach(name -> { + outputView.printExcludedFood(name); + menuService.addCoach(name, inputView.readExcludedFood()); + outputView.printNewLine(); + }); + } +} diff --git a/src/main/java/menu/domain/Categories.java b/src/main/java/menu/domain/Categories.java new file mode 100644 index 000000000..7e10322e0 --- /dev/null +++ b/src/main/java/menu/domain/Categories.java @@ -0,0 +1,58 @@ +package menu.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import menu.constant.Category; +import menu.constant.ResultElement; + +import java.util.*; + +public class Categories { + + private static final int RECOMMEND_NUMBER = 5; + private static final int MAX_RECOMMEND_COUNT = 2; + private static final String TITLE = "카테고리"; + + private final List categories; + private final Map categoriesCount; + + public Categories() { + this.categoriesCount = new EnumMap<>(Category.class); + Arrays.stream(Category.values()) + .forEach(category -> categoriesCount.put(category, 0)); + this.categories = recommendCategory(); + } + + private List recommendCategory() { + List categories = new ArrayList<>(); + while (categories.size() < RECOMMEND_NUMBER) { + Category recommendCategory = Category.getRecommendFood(Randoms.pickNumberInRange(1, 5)); + if (isNotThreeTimes(recommendCategory)) { + categories.add(recommendCategory); + categoriesCount.put(recommendCategory, increaseCount(recommendCategory)); + } + } + return List.copyOf(categories); + } + + private boolean isNotThreeTimes(Category recommendCategory) { + return categoriesCount.get(recommendCategory) < MAX_RECOMMEND_COUNT; + } + + private int increaseCount(Category category) { + return categoriesCount.get(category) + 1; + } + + public List getCategories() { + return categories; + } + + @Override + public String toString() { + StringJoiner stringJoiner = new StringJoiner(ResultElement.DELIMITER.toString(), ResultElement.START.toString(), ResultElement.END.toString()); + stringJoiner.add(TITLE); + categories.forEach( + (category) -> stringJoiner.add(category.toString()) + ); + return stringJoiner.toString(); + } +} diff --git a/src/main/java/menu/domain/Coach.java b/src/main/java/menu/domain/Coach.java new file mode 100644 index 000000000..914f8b7df --- /dev/null +++ b/src/main/java/menu/domain/Coach.java @@ -0,0 +1,32 @@ +package menu.domain; + +import menu.constant.Category; +import menu.constant.ResultElement; + +import java.util.List; +import java.util.StringJoiner; + +public class Coach { + + private final Name name; + private final ExcludedMenu excludedMenu; + private final RecommendMenu recommendMenu; + + public Coach(Name name, List excludedFoods) { + this.name = name; + this.excludedMenu = new ExcludedMenu(excludedFoods); + this.recommendMenu = new RecommendMenu(); + } + + public void recommendFoods(Category category) { + recommendMenu.recommendFoods(category, excludedMenu); + } + + @Override + public String toString() { + StringJoiner stringJoiner = new StringJoiner(ResultElement.DELIMITER.toString(), ResultElement.START.toString(), ResultElement.END.toString()); + stringJoiner.add(name.toString()); + stringJoiner.add(recommendMenu.toString()); + return stringJoiner.toString(); + } +} diff --git a/src/main/java/menu/domain/Coaches.java b/src/main/java/menu/domain/Coaches.java new file mode 100644 index 000000000..4f4603f19 --- /dev/null +++ b/src/main/java/menu/domain/Coaches.java @@ -0,0 +1,27 @@ +package menu.domain; + +import menu.constant.Category; + +import java.util.ArrayList; +import java.util.List; + +public class Coaches { + + private static final String NEW_LINE = "\n"; + private final List coaches = new ArrayList<>(); + + public void addCoach(Name name, List excludedFoods) { + coaches.add(new Coach(name, excludedFoods)); + } + + public void recommendMenu(Category category) { + coaches.forEach(coach -> coach.recommendFoods(category)); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + coaches.forEach(coach -> stringBuilder.append(coach.toString()).append(NEW_LINE)); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/menu/domain/ExcludedMenu.java b/src/main/java/menu/domain/ExcludedMenu.java new file mode 100644 index 000000000..2c3403205 --- /dev/null +++ b/src/main/java/menu/domain/ExcludedMenu.java @@ -0,0 +1,17 @@ +package menu.domain; + +import java.util.List; + +public class ExcludedMenu { + + private final List excludedMenus; + + + public ExcludedMenu(List excludedMenus) { + this.excludedMenus = excludedMenus; + } + + public boolean isExcludedMenu(String recommendMenu) { + return excludedMenus.contains(recommendMenu); + } +} diff --git a/src/main/java/menu/domain/Name.java b/src/main/java/menu/domain/Name.java new file mode 100644 index 000000000..7fa36c040 --- /dev/null +++ b/src/main/java/menu/domain/Name.java @@ -0,0 +1,28 @@ +package menu.domain; + +import menu.constant.ExceptionMessage; + +public class Name { + + private static final int MIN_NAME_LENGTH = 2; + private static final int MAX_NAME_LENGTH = 4; + + private final String name; + + public Name(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (!(MIN_NAME_LENGTH <= name.length() && name.length() <= MAX_NAME_LENGTH)) { + ExceptionMessage exceptionMessage = ExceptionMessage.INCORRECT_NAME_RANGE; + throw new IllegalArgumentException(exceptionMessage.toString()); + } + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/menu/domain/RecommendMenu.java b/src/main/java/menu/domain/RecommendMenu.java new file mode 100644 index 000000000..cc02b1ae2 --- /dev/null +++ b/src/main/java/menu/domain/RecommendMenu.java @@ -0,0 +1,40 @@ +package menu.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import menu.constant.Category; +import menu.constant.Menu; +import menu.constant.ResultElement; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +public class RecommendMenu { + + private static final int WINNING_NUMBER = 0; + + private final List menus = new ArrayList<>(); + + public void recommendFoods(Category category, ExcludedMenu excludedMenu) { + menus.add(getRandomFood(category, excludedMenu)); + } + + private String getRandomFood(Category category, ExcludedMenu excludedMenu) { + String menu = Randoms.shuffle(Menu.getFoodsByCategory(category)).get(WINNING_NUMBER); + while (isDuplicateRecommend(menu) || excludedMenu.isExcludedMenu(menu)) { + menu = Randoms.shuffle(Menu.getFoodsByCategory(category)).get(WINNING_NUMBER); + } + return menu; + } + + private boolean isDuplicateRecommend(String food) { + return menus.contains(food); + } + + @Override + public String toString() { + StringJoiner stringJoiner = new StringJoiner(ResultElement.DELIMITER.toString()); + menus.forEach(stringJoiner::add); + return stringJoiner.toString(); + } +} diff --git a/src/main/java/menu/service/MenuService.java b/src/main/java/menu/service/MenuService.java new file mode 100644 index 000000000..25779aa9b --- /dev/null +++ b/src/main/java/menu/service/MenuService.java @@ -0,0 +1,36 @@ +package menu.service; + +import menu.constant.Category; +import menu.domain.Categories; +import menu.domain.Coaches; +import menu.domain.Name; + +import java.util.List; + +public class MenuService { + + private final Coaches coaches = new Coaches(); + private final Categories categories = new Categories(); + + public void addCoach(Name name, List excludedFoods) { + coaches.addCoach(name, excludedFoods); + } + + + public void recommendMenus() { + categories.getCategories() + .forEach(this::recommendMenu); + } + + private void recommendMenu(Category category) { + coaches.recommendMenu(category); + } + + public Categories getCategories() { + return categories; + } + + public Coaches getCoaches() { + return coaches; + } +} diff --git a/src/main/java/menu/validator/InputValidator.java b/src/main/java/menu/validator/InputValidator.java new file mode 100644 index 000000000..9067979dd --- /dev/null +++ b/src/main/java/menu/validator/InputValidator.java @@ -0,0 +1,17 @@ +package menu.validator; + +import menu.constant.ExceptionMessage; + +import java.util.regex.Pattern; + +public class InputValidator { + + private static final String DELIMITER_REGEXP = "(\\S|)+(,\\S)?"; + + public void validateDelimiter(String input) { + if (!Pattern.matches(DELIMITER_REGEXP, input)) { + ExceptionMessage exceptionMessage = ExceptionMessage.INCORRECT_DELIMITER; + throw new IllegalArgumentException(exceptionMessage.toString()); + } + } +} diff --git a/src/main/java/menu/view/InputView.java b/src/main/java/menu/view/InputView.java new file mode 100644 index 000000000..00683a82a --- /dev/null +++ b/src/main/java/menu/view/InputView.java @@ -0,0 +1,47 @@ +package menu.view; + +import camp.nextstep.edu.missionutils.Console; +import menu.domain.Name; +import menu.validator.InputValidator; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class InputView { + + InputValidator inputValidator = new InputValidator(); + OutputView outputView = new OutputView(); + + public List readNames() { + return attemptedInput(() -> { + outputView.printNameMessage(); + String names = Console.readLine(); + outputView.printNewLine(); + inputValidator.validateDelimiter(names); + return Arrays.stream(names.split(",")) + .map(Name::new) + .collect(Collectors.toList()); + }); + } + + public List readExcludedFood() { + return attemptedInput(() -> { + String foods = Console.readLine(); + inputValidator.validateDelimiter(foods); + return Stream.of(foods.split(",")) + .collect(Collectors.toList()); + }); + } + + private T attemptedInput(Supplier supplier) { + try { + return supplier.get(); + } catch (IllegalArgumentException exception) { + outputView.printExceptionMessage(exception.getMessage()); + return supplier.get(); + } + } +} diff --git a/src/main/java/menu/view/OutputView.java b/src/main/java/menu/view/OutputView.java new file mode 100644 index 000000000..af3448944 --- /dev/null +++ b/src/main/java/menu/view/OutputView.java @@ -0,0 +1,68 @@ +package menu.view; + +import menu.constant.Days; +import menu.constant.OutputMessage; +import menu.constant.ResultElement; +import menu.domain.Categories; +import menu.domain.Coaches; +import menu.domain.Name; + +import java.util.Arrays; +import java.util.StringJoiner; + +public class OutputView { + + public void printStartMessage() { + System.out.println(OutputMessage.START_MESSAGE); + printNewLine(); + } + + public void printNameMessage() { + System.out.println(OutputMessage.COACH_NAME); + } + + public void printExcludedFood(Name name) { + System.out.printf(OutputMessage.EXCLUDED_FOOD.toString(), name); + + } + + public void printResult(Categories categories, Coaches coaches) { + printMenuResultMessage(); + printDays(); + printCategory(categories); + printRecommendResult(coaches); + printComplete(); + } + + private void printMenuResultMessage() { + System.out.println(OutputMessage.MENU_RESULT); + } + + private void printDays() { + StringJoiner stringJoiner = new StringJoiner(ResultElement.DELIMITER.toString(), ResultElement.START.toString(), ResultElement.END.toString()); + stringJoiner.add(OutputMessage.DAYS_TITLE.toString()); + Arrays.stream(Days.values()) + .forEach((day) -> stringJoiner.add(day.toString())); + System.out.println(stringJoiner); + } + + private void printCategory(Categories categories) { + System.out.println(categories); + } + + private void printRecommendResult(Coaches coaches) { + System.out.println(coaches); + } + + private void printComplete() { + System.out.println(OutputMessage.COMPLETE); + } + + public void printNewLine() { + System.out.println(); + } + + public void printExceptionMessage(String exceptionMessage) { + System.out.println(exceptionMessage); + } +} diff --git a/src/test/java/menu/CategoriesTest.java b/src/test/java/menu/CategoriesTest.java new file mode 100644 index 000000000..01657520a --- /dev/null +++ b/src/test/java/menu/CategoriesTest.java @@ -0,0 +1,23 @@ +package menu; + +import menu.domain.Categories; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CategoriesTest { + + @Test + void recommendCategories() { + Categories categories = new Categories(); + + assertThat(categories.getCategories().size()).isEqualTo(5); + } + + @Test + void categoriesToString() { + Categories categories = new Categories(); + + assertThat(categories.toString()).contains("[ 카테고리 |"); + } +} diff --git a/src/test/java/menu/CategoryTest.java b/src/test/java/menu/CategoryTest.java new file mode 100644 index 000000000..8645eca2e --- /dev/null +++ b/src/test/java/menu/CategoryTest.java @@ -0,0 +1,44 @@ +package menu; + +import menu.constant.Category; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CategoryTest { + + @Test + void getJapanese() { + Category category = Category.getRecommendFood(1); + + assertThat(category).isEqualTo(Category.JAPANESE); + } + + @Test + void getKorean() { + Category category = Category.getRecommendFood(2); + + assertThat(category).isEqualTo(Category.KOREAN); + } + + @Test + void getChinese() { + Category category = Category.getRecommendFood(3); + + assertThat(category).isEqualTo(Category.CHINESE); + } + + @Test + void getAsian() { + Category category = Category.getRecommendFood(4); + + assertThat(category).isEqualTo(Category.ASIAN); + } + + @Test + void getWestern() { + Category category = Category.getRecommendFood(5); + + assertThat(category).isEqualTo(Category.WESTERN); + } +} diff --git a/src/test/java/menu/InputValidatorTest.java b/src/test/java/menu/InputValidatorTest.java new file mode 100644 index 000000000..13fa81f3d --- /dev/null +++ b/src/test/java/menu/InputValidatorTest.java @@ -0,0 +1,28 @@ +package menu; + +import menu.validator.InputValidator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class InputValidatorTest { + + InputValidator inputValidator = new InputValidator(); + String ERROR_MESSAGE = "[ERROR]"; + + @Test + void validateSpace() { + String input = "토미, 제임스, 포코"; + assertThatThrownBy(() -> inputValidator.validateDelimiter(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } + + @Test + void validateDelimiter() { + String input = "토미| 제임스, 포코"; + assertThatThrownBy(() -> inputValidator.validateDelimiter(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } +} diff --git a/src/test/java/menu/NameTest.java b/src/test/java/menu/NameTest.java new file mode 100644 index 000000000..2bfb19872 --- /dev/null +++ b/src/test/java/menu/NameTest.java @@ -0,0 +1,27 @@ +package menu; + +import menu.domain.Name; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class NameTest { + + String ERROR_MESSAGE = "[ERROR]"; + + @Test + void validateMinLength() { + String input = "빵"; + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } + + @Test + void validateMaxLength() { + String input = "토미제임스포코"; + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } +}