diff --git a/docs/README.md b/docs/README.md index e69de29bb..32dd65890 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,23 @@ +## 기능 목록 + +- [x] 추천 시작 안내 메시지 출력 + +- [x] 코치의 이름 입력 + - [x] `,`로 나누지 않은 경우 예외 발생 + - [x] 각 이름은 2~4글자가 아닐 경우 예외 발생 + - [x] 코치 수가 2명~5명이 아닐 경우 예외 발생 + +- [x] 각 코치마다 못 먹는 음식 입력 + - [x] 0~2개가 아닐 경우 예외 + +- [x] 카테고리 추천 + - [x] 월~금까지 먹을 카테고리를 추천한다. + - [x] 이미 2번 추천된 카테고리일 경우 재추천 + +- [x] 코치 별 음식 추천 + - [x] 요일마다 코치가 먹을 음식을 추천한다. + - [x] 이미 한 번 먹은 음식일 경우 재추천 + - [x] 코치가 못먹는 음식일 경우 재추천 + +- [x] 추천 결과 출력 +- [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/Days.java b/src/main/java/menu/constant/Days.java new file mode 100644 index 000000000..b588b6212 --- /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; + } +} \ No newline at end of file diff --git a/src/main/java/menu/constant/ExceptionMessage.java b/src/main/java/menu/constant/ExceptionMessage.java new file mode 100644 index 000000000..0c66010fd --- /dev/null +++ b/src/main/java/menu/constant/ExceptionMessage.java @@ -0,0 +1,22 @@ +package menu.constant; + +public enum ExceptionMessage { + + INCORRECT_DELIMITER("','로 구분하여 입력해야합니다."), + INCORRECT_NAME_RANGE("코치의 이름은 2~4자까지 입력할 수 있습니다."), + INCORRECT_COACH_NUMBER("코치는 2명 이상, 5명 이하만 입력할 수 있습니다."), + INCORRECT_RECOMMEND("등록되지 않은 카테고리입니다."), + EXCLUDED_FOOD_SIZE("못먹는 음식은 0~2개까지 입력가능합니다."); + + 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/OutputMessage.java b/src/main/java/menu/constant/OutputMessage.java new file mode 100644 index 000000000..ab8e84a37 --- /dev/null +++ b/src/main/java/menu/constant/OutputMessage.java @@ -0,0 +1,22 @@ +package menu.constant; + +public enum OutputMessage { + + START_RECOMMEND("점심 메뉴 추천을 시작합니다."), + READ_NAMES("코치의 이름을 입력해 주세요. (, 로 구분)"), + READ_EXCLUDED_MENU("%s(이)가 못 먹는 메뉴를 입력해 주세요."), + RECOMMEND_RESULT("메뉴 추천 결과입니다."), + COMPLETE_RECOMMEND("추천을 완료했습니다."), + DAYS("구분"), + CATEGORIES("카테고리"); + + private final String message; + + OutputMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/menu/constant/ResultElement.java b/src/main/java/menu/constant/ResultElement.java new file mode 100644 index 000000000..eee719990 --- /dev/null +++ b/src/main/java/menu/constant/ResultElement.java @@ -0,0 +1,21 @@ +package menu.constant; + +public enum ResultElement { + + START("[ "), + END(" ]"), + DELIMITER(" | "), + NEW_LINE("\n"); + + + 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..ebc042383 --- /dev/null +++ b/src/main/java/menu/controller/MenuController.java @@ -0,0 +1,38 @@ +package menu.controller; + +import menu.domain.Coaches; +import menu.service.MenuService; +import menu.view.InputView; +import menu.view.OutputView; + +public class MenuController { + + private final OutputView outputView = new OutputView(); + private final InputView inputView = new InputView(); + + public void start() { + outputView.printStart(); + MenuService menuService = new MenuService(readCoaches()); + menuService.recommend(); + outputView.printRecommendResult(menuService.getCategories(), menuService.getCoaches()); + } + + private Coaches readCoaches() { + Coaches coaches = readNames(); + readExcludedFood(coaches); + return coaches; + } + + private Coaches readNames() { + outputView.printCoachNames(); + return inputView.readNames(); + } + + private void readExcludedFood(Coaches coaches) { + coaches.getCoaches() + .forEach(coach -> { + outputView.printExcludedMenu(coach.getName()); + coach.addExcludedMenu(inputView.readExcludedMenu()); + }); + } +} diff --git a/src/main/java/menu/domain/Categories.java b/src/main/java/menu/domain/Categories.java new file mode 100644 index 000000000..f173af650 --- /dev/null +++ b/src/main/java/menu/domain/Categories.java @@ -0,0 +1,60 @@ +package menu.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import menu.constant.OutputMessage; +import menu.constant.ResultElement; + +import java.util.*; + +public class Categories { + + private static final int RECOMMEND_COUNT = 5; + private static final int MAX_RECOMMEND_COUNT = 2; + private static final int START_RANDOM_NUMBER = 1; + private static final int END_RANDOM_NUMBER = 5; + + private final List elements; + + public Categories() { + this.elements = recommendCategory(); + } + + private List recommendCategory() { + List categories = new ArrayList<>(); + while (categories.size() < RECOMMEND_COUNT) { + Category category = randomRecommend(); + if (isDuplicateRecommend(categories, category)) { + continue; + } + categories.add(category); + } + return categories; + } + + private Category randomRecommend() { + int random = Randoms.pickNumberInRange(START_RANDOM_NUMBER, END_RANDOM_NUMBER); + return Category.getRecommendFood(random); + } + + private boolean isDuplicateRecommend(List categories, Category recommendCategory) { + return categories.stream() + .filter(category -> category == recommendCategory) + .count() >= MAX_RECOMMEND_COUNT; + } + + public List getElements() { + return Collections.unmodifiableList(elements); + } + + public String getCategories() { + String delimiter = ResultElement.DELIMITER.toString(); + String start = ResultElement.START.toString(); + String end = ResultElement.END.toString(); + + StringJoiner stringJoiner = new StringJoiner(delimiter, start, end); + + stringJoiner.add(OutputMessage.CATEGORIES.getMessage()); + elements.forEach(element -> stringJoiner.add(element.toString())); + return stringJoiner.toString(); + } +} diff --git a/src/main/java/menu/domain/Category.java b/src/main/java/menu/domain/Category.java new file mode 100644 index 000000000..e8717e220 --- /dev/null +++ b/src/main/java/menu/domain/Category.java @@ -0,0 +1,34 @@ +package menu.domain; + +import menu.constant.ExceptionMessage; + +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/domain/Coach.java b/src/main/java/menu/domain/Coach.java new file mode 100644 index 000000000..a03b1b527 --- /dev/null +++ b/src/main/java/menu/domain/Coach.java @@ -0,0 +1,40 @@ +package menu.domain; + +import menu.constant.ResultElement; + +import java.util.StringJoiner; + +public class Coach { + + private final Name name; + + private final RecommendMenu recommendMenu = new RecommendMenu(); + + public Coach(Name name) { + this.name = name; + } + + public void recommendMenu(Category category) { + recommendMenu.recommend(category); + } + + public String getName() { + return name.getValue(); + } + + public String getRecommendMenu() { + String delimiter = ResultElement.DELIMITER.toString(); + String start = ResultElement.START.toString(); + String end = ResultElement.END.toString(); + + StringJoiner stringJoiner = new StringJoiner(delimiter, start, end); + + stringJoiner.add(name.getValue()); + stringJoiner.add(recommendMenu.getMenus()); + return stringJoiner.toString(); + } + + public void addExcludedMenu(ExcludedMenu excludedMenu) { + recommendMenu.addExcludedMenu(excludedMenu); + } +} diff --git a/src/main/java/menu/domain/Coaches.java b/src/main/java/menu/domain/Coaches.java new file mode 100644 index 000000000..ac534143f --- /dev/null +++ b/src/main/java/menu/domain/Coaches.java @@ -0,0 +1,35 @@ +package menu.domain; + +import menu.constant.ResultElement; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class Coaches { + + private final List coaches; + + public Coaches(Names names) { + this.coaches = names.getElements() + .stream() + .map(Coach::new) + .collect(Collectors.toList()); + } + + public void recommendByCategory(Category category) { + coaches.forEach(coach -> coach.recommendMenu(category)); + } + + public List getCoaches() { + return Collections.unmodifiableList(coaches); + } + + public String getRecommend() { + StringBuilder stringBuilder = new StringBuilder(); + coaches.forEach(coach -> + stringBuilder.append(coach.getRecommendMenu()) + .append(ResultElement.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..f613e80c7 --- /dev/null +++ b/src/main/java/menu/domain/ExcludedMenu.java @@ -0,0 +1,28 @@ +package menu.domain; + +import menu.constant.ExceptionMessage; + +import java.util.ArrayList; +import java.util.List; + +public class ExcludedMenu { + + private static final int MAX_SIZE = 2; + + private final List elements; + + public ExcludedMenu(List elements) { + validateSize(elements); + this.elements = new ArrayList<>(elements); + } + + private void validateSize(List menus) { + if (!(menus.size() <= MAX_SIZE)) { + throw new IllegalArgumentException(ExceptionMessage.EXCLUDED_FOOD_SIZE.toString()); + } + } + + public boolean canEat(String menu) { + return !(elements.contains(menu)); + } +} diff --git a/src/main/java/menu/domain/Menu.java b/src/main/java/menu/domain/Menu.java new file mode 100644 index 000000000..94d158677 --- /dev/null +++ b/src/main/java/menu/domain/Menu.java @@ -0,0 +1,29 @@ +package menu.domain; + +import menu.constant.ExceptionMessage; + +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 getMenuByCategory(Category category) { + return Arrays.stream(Menu.values()) + .filter(value -> value.category == category) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(ExceptionMessage.INCORRECT_RECOMMEND.toString())) + .foods; + } +} \ No newline at end of file diff --git a/src/main/java/menu/domain/Name.java b/src/main/java/menu/domain/Name.java new file mode 100644 index 000000000..1b8a1d254 --- /dev/null +++ b/src/main/java/menu/domain/Name.java @@ -0,0 +1,26 @@ +package menu.domain; + +import menu.constant.ExceptionMessage; + +public class Name { + + private static final int MIN_LENGTH = 2; + private static final int MAX_LENGTH = 4; + + private final String value; + + public Name(String value) { + validateLength(value); + this.value = value; + } + + public void validateLength(String value) { + if (!(MIN_LENGTH <= value.length() && value.length() <= MAX_LENGTH)) { + throw new IllegalArgumentException(ExceptionMessage.INCORRECT_NAME_RANGE.toString()); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/menu/domain/Names.java b/src/main/java/menu/domain/Names.java new file mode 100644 index 000000000..053841e4c --- /dev/null +++ b/src/main/java/menu/domain/Names.java @@ -0,0 +1,30 @@ +package menu.domain; + +import menu.constant.ExceptionMessage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Names { + + private static final int MIN_RANGE = 2; + private static final int MAX_RANGE = 5; + + private final List elements; + + public Names(List names) { + validateRange(names); + this.elements = new ArrayList<>(names); + } + + private void validateRange(List names) { + if (!(MIN_RANGE <= names.size() && names.size() <= MAX_RANGE)) { + throw new IllegalArgumentException(ExceptionMessage.INCORRECT_COACH_NUMBER.toString()); + } + } + + public List getElements() { + return Collections.unmodifiableList(elements); + } +} \ No newline at end of file diff --git a/src/main/java/menu/domain/RecommendMenu.java b/src/main/java/menu/domain/RecommendMenu.java new file mode 100644 index 000000000..c98cd7090 --- /dev/null +++ b/src/main/java/menu/domain/RecommendMenu.java @@ -0,0 +1,43 @@ +package menu.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import menu.constant.ResultElement; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +public class RecommendMenu { + + private static final int RECOMMEND_NUMBER = 0; + + private final List menus = new ArrayList<>(); + private ExcludedMenu excludedMenu; + + public void addExcludedMenu(ExcludedMenu excludedMenu) { + this.excludedMenu = excludedMenu; + } + + public void recommend(Category category) { + String menu = Randoms.shuffle(Menu.getMenuByCategory(category)).get(RECOMMEND_NUMBER); + while (isDuplicate(menu) || isExcludedMenu(menu)) { + menu = Randoms.shuffle(Menu.getMenuByCategory(category)).get(RECOMMEND_NUMBER); + } + menus.add(menu); + } + + private boolean isDuplicate(String menu) { + return menus.contains(menu); + } + + private boolean isExcludedMenu(String menu) { + return !excludedMenu.canEat(menu); + } + + public String getMenus() { + 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..1abec2971 --- /dev/null +++ b/src/main/java/menu/service/MenuService.java @@ -0,0 +1,28 @@ +package menu.service; + +import menu.domain.Categories; +import menu.domain.Coaches; + +public class MenuService { + + private final Coaches coaches; + private final Categories categories; + + public MenuService(Coaches coaches) { + this.coaches = coaches; + this.categories = new Categories(); + } + + public void recommend() { + categories.getElements() + .forEach(coaches::recommendByCategory); + } + + 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..427d2dfdc --- /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 = "^[a-zA-Zㄱ-힣0-9,]*$"; + + 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..392f87674 --- /dev/null +++ b/src/main/java/menu/view/InputView.java @@ -0,0 +1,54 @@ +package menu.view; + +import camp.nextstep.edu.missionutils.Console; +import menu.domain.Coaches; +import menu.domain.ExcludedMenu; +import menu.domain.Name; +import menu.domain.Names; +import menu.validator.InputValidator; + +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class InputView { + + private static final String REGEX = ","; + + private final InputValidator inputValidator = new InputValidator(); + + public Coaches readNames() { + return attemptedInput( + () -> { + String input = Console.readLine(); + inputValidator.validateDelimiter(input); + List names = Stream.of(input.split(REGEX)) + .map(Name::new) + .collect(Collectors.toList()); + return new Coaches(new Names(names)); + } + ); + } + + public ExcludedMenu readExcludedMenu() { + return attemptedInput( + () -> { + String input = Console.readLine(); + inputValidator.validateDelimiter(input); + List excludedMenu = Stream.of(input.split(REGEX)) + .collect(Collectors.toList()); + return new ExcludedMenu(excludedMenu); + } + ); + } + + private T attemptedInput(Supplier supplier) { + try { + return supplier.get(); + } catch (IllegalArgumentException exception) { + System.out.println(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..c642ced10 --- /dev/null +++ b/src/main/java/menu/view/OutputView.java @@ -0,0 +1,67 @@ +package menu.view; + +import menu.constant.Days; +import menu.constant.OutputMessage; +import menu.constant.ResultElement; +import menu.domain.Categories; +import menu.domain.Coaches; + +import java.util.Arrays; +import java.util.StringJoiner; + +public class OutputView { + + public void printStart() { + System.out.println(OutputMessage.START_RECOMMEND.getMessage()); + printNewLine(); + } + + public void printCoachNames() { + System.out.println(OutputMessage.READ_NAMES.getMessage()); + } + + public void printNewLine() { + System.out.println(); + } + + public void printExcludedMenu(String name) { + System.out.printf(OutputMessage.READ_EXCLUDED_MENU.getMessage(), name); + printNewLine(); + } + + public void printRecommendResult(Categories categories, Coaches coaches) { + printResultMessage(); + printDays(); + printCategories(categories); + printCoaches(coaches); + printCompleteMessage(); + } + + private void printResultMessage() { + System.out.println(OutputMessage.RECOMMEND_RESULT.getMessage()); + } + + private void printDays() { + StringJoiner stringJoiner = new StringJoiner(ResultElement.DELIMITER.toString(), ResultElement.START.toString(), ResultElement.END.toString()); + stringJoiner.add(OutputMessage.DAYS.getMessage()); + Arrays.stream(Days.values()) + .forEach((day) -> stringJoiner.add(day.toString())); + System.out.println(stringJoiner); + } + + private void printCategories(Categories categories) { + System.out.println(categories.getCategories()); + } + + private void printCoaches(Coaches coaches) { + System.out.println(coaches.getRecommend()); + } + + private void printCompleteMessage() { + System.out.println(OutputMessage.COMPLETE_RECOMMEND.getMessage()); + } + + public void printExceptionMessage(String message) { + System.out.println(message); + } +} diff --git a/src/test/java/menu/domain/NameTest.java b/src/test/java/menu/domain/NameTest.java new file mode 100644 index 000000000..5806b0179 --- /dev/null +++ b/src/test/java/menu/domain/NameTest.java @@ -0,0 +1,30 @@ +package menu.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class NameTest { + private static final String ERROR_MESSAGE = "[ERROR]"; + + @DisplayName("이름의 길이가 4가 넘어갈 경우 예외가 발생한다.") + @Test + void nameMaxLengthException() { + String name = "김연진김연진"; + + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } + + @DisplayName("이름의 길이가 1이하일 경우 예외가 발생한다.") + @Test + void nameMinLengthException() { + String name = "김"; + + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } +} diff --git a/src/test/java/menu/domain/NamesTest.java b/src/test/java/menu/domain/NamesTest.java new file mode 100644 index 000000000..e4be57571 --- /dev/null +++ b/src/test/java/menu/domain/NamesTest.java @@ -0,0 +1,41 @@ +package menu.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class NamesTest { + + private static final String ERROR_MESSAGE = "[ERROR]"; + + @DisplayName("코치 수가 1이하일 경우 예외가 발생한다.") + @Test + void minCaochRangeException() { + Name name = new Name("김연진"); + + assertThatThrownBy(() -> new Names(List.of(name))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } + + @DisplayName("코치 수가 1이하일 경우 예외가 발생한다.") + @Test + void maxCaochRangeException() { + Name name1 = new Name("김연진"); + Name name2 = new Name("토미"); + Name name3 = new Name("제임스"); + Name name4 = new Name("포크"); + Name name5 = new Name("이름"); + Name name6 = new Name("닉네임"); + + List names = List.of(name1, name2, name3, name4, name5, name6); + + assertThatThrownBy(() -> new Names(names)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(ERROR_MESSAGE); + } +}