diff --git a/docs/how-to-solve.md b/docs/how-to-solve.md index cfa1477a..2835e1be 100644 --- a/docs/how-to-solve.md +++ b/docs/how-to-solve.md @@ -1,5 +1,35 @@ ### 미션 해결 전략 #### 1. 본인이 이해하고 구현한 내용에 기반해 '다른 근무자와 순서를 바꿔야 하는 경우'를 자신만의 예시를 들어 설명하세요. (필수) +우선 평일순번과 주말순번을 돌다보면 겹치는 상황이 발생할 것 이고, 요구사항에 따라 다음 순서의 근무자와 교체 후 당번을 만들면 된다고 생각했습니다. +가장 쉬운 예시로 +휴일이 없는 4,월 을 기준으로 +4,월 +평일 비상 근무 순번대로 사원 닉네임을 입력하세요> 준팍,도밥,고니,수아,루루,글로,솔로스타 +휴일 비상 근무 순번대로 사원 닉네임을 입력하세요> 준팍,도밥,고니,수아,루루,글로,솔로스타 + +이러한 형태로 입력하게 되면 +1. 처음 평일 순번 준팍 ~ 루루 +2. 처음 주말 순번 준팍, 도밥 +3. 그 다음 평일 순번 글로 ~ 고니 +4. 금요일이 고니가 되는데 고니는 토요일순번이 되어버려서 근무가 겹칩니다. +5. 그래서 이때 고니와 수아의 주말순번을 변경하고 +4월 1일 월 준팍 +4월 2일 화 도밥 +4월 3일 수 고니 +4월 4일 목 수아 +4월 5일 금 루루 +4월 6일 토 준팍 +4월 7일 일 도밥 +4월 8일 월 글로 +4월 9일 화 솔로스타 +4월 10일 수 준팍 +4월 11일 목 도밥 +4월 12일 금 고니 +4월 13일 토 수아 -> 원래 고니 순번 +4월 14일 일 고니 -> 원래 수아 순번 +4월 15일 월 수아 +위와 같은 형태가 됩니다. +주말에서 평일이 넘어가는 경우도 동일하게 평일순번 위치를 변경하면 문제를 해결할 수 있습니다. #### 2. 요구사항에서 제시한 앞의 날짜부터 순서를 변경하는 방법 외에 다른 방법이 있다면 어떤 방식이 있는지, 이 방법은 기존에 제시된 방식과 비교해 어떤 차이가 있는지 설명하세요. (선택) diff --git a/src/main/java/oncall/Application.java b/src/main/java/oncall/Application.java index ebf57861..8a6e162f 100644 --- a/src/main/java/oncall/Application.java +++ b/src/main/java/oncall/Application.java @@ -1,7 +1,17 @@ package oncall; +import oncall.controller.CustomController; +import oncall.service.CustomService; +import oncall.view.InputView; +import oncall.view.OutputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + CustomService customService = new CustomService(); + + CustomController controller = new CustomController(inputView, outputView, customService); + controller.run(); } } diff --git a/src/main/java/oncall/config/Holidays.java b/src/main/java/oncall/config/Holidays.java new file mode 100644 index 00000000..b77a1705 --- /dev/null +++ b/src/main/java/oncall/config/Holidays.java @@ -0,0 +1,29 @@ +package oncall.config; + +public enum Holidays { + NEW_YEARS_DAY(1, 1), // 1월 1일 + INDEPENDENCE_DAY(3, 1), // 3월 1일 + CHILDRENS_DAY(5, 5), // 5월 5일 + MEMORIAL_DAY(6, 6), // 6월 6일 + LIBERATION_DAY(8, 15), // 8월 15일 + NATIONAL_FOUNDATION_DAY(10, 3), // 10월 3일 + HANGUL_DAY(10, 9), // 10월 9일 + CHRISTMAS_DAY(12, 25); // 12월 25일 + + private final int month; + private final int day; + + // Constructor + Holidays(int month, int day) { + this.month = month; + this.day = day; + } + + public int getMonth() { + return month; + } + + public int getDay() { + return day; + } +} diff --git a/src/main/java/oncall/controller/CustomController.java b/src/main/java/oncall/controller/CustomController.java new file mode 100644 index 00000000..6d8f024b --- /dev/null +++ b/src/main/java/oncall/controller/CustomController.java @@ -0,0 +1,44 @@ +package oncall.controller; + +import java.util.List; +import oncall.domain.CombinedRoster; +import oncall.domain.MonthAndDayOfWeek; +import oncall.domain.MonthlyRoster; +import oncall.domain.Roster; +import oncall.domain.Worker; +import oncall.service.CustomService; +import oncall.view.InputView; +import oncall.view.OutputView; + +public class CustomController extends ExceptionLoopController{ + private final InputView input; + private final OutputView output; + private final CustomService service; + + public CustomController(InputView input, OutputView output, CustomService service) { + this.input = input; + this.output = output; + this.service = service; + } + + public void run() { + MonthAndDayOfWeek monthAndDayOfWeek = repeatUntilValid(this::getMonthAndDayOfWeek); + CombinedRoster combinedRoster = repeatUntilValid(this::getRoster); + List monthlyRoster = service.makeMonthlyRoster(monthAndDayOfWeek, combinedRoster); + output.printMonthlyRoster(monthlyRoster); + } + + private MonthAndDayOfWeek getMonthAndDayOfWeek() { + output.printGetMonthDayOfWeek(); + return input.getMonthDayOfWeek(); + } + + private CombinedRoster getRoster() { + output.printGetWeekdayRoster(); + Roster weekdayRoster = input.getRoster(); + output.printGetWeekendRoster(); + Roster weekendRoster = input.getRoster(); + + return new CombinedRoster(weekdayRoster, weekendRoster); + } +} diff --git a/src/main/java/oncall/controller/ExceptionLoopController.java b/src/main/java/oncall/controller/ExceptionLoopController.java new file mode 100644 index 00000000..ae42938d --- /dev/null +++ b/src/main/java/oncall/controller/ExceptionLoopController.java @@ -0,0 +1,16 @@ +package oncall.controller; + +import java.util.function.Supplier; +import oncall.exception.CustomException; + +public abstract class ExceptionLoopController { + protected T repeatUntilValid(Supplier function) { + while(true) { + try { + return function.get(); + } catch (CustomException e) { + System.out.println(e.getMessage()); + } + } + } +} diff --git a/src/main/java/oncall/domain/CombinedRoster.java b/src/main/java/oncall/domain/CombinedRoster.java new file mode 100644 index 00000000..ec9315eb --- /dev/null +++ b/src/main/java/oncall/domain/CombinedRoster.java @@ -0,0 +1,34 @@ +package oncall.domain; + +import java.util.List; +import oncall.exception.IllegalInputException; + +public class CombinedRoster { + private final Roster weekdayRoster; + private final Roster weekendRoster; + + public CombinedRoster(Roster weekdayRoster, Roster weekendRoster) { + validate(weekdayRoster, weekendRoster); + this.weekdayRoster = weekdayRoster; + this.weekendRoster = weekendRoster; + } + + private void validate(Roster weekdayRoster, Roster weekendRoster) { + List weekdayWorkers = weekdayRoster.getWorkers(); + List weekendWorkers = weekendRoster.getWorkers(); + + if(weekdayWorkers.size() != weekendWorkers.size()){ + throw new IllegalInputException(); + } + // 중복 검사 로직은 나중에 + } + + public Roster getWeekdayRoster() { + return weekdayRoster; + } + + public Roster getWeekendRoster() { + return weekendRoster; + } + +} diff --git a/src/main/java/oncall/domain/MonthAndDayOfWeek.java b/src/main/java/oncall/domain/MonthAndDayOfWeek.java new file mode 100644 index 00000000..ef9be63f --- /dev/null +++ b/src/main/java/oncall/domain/MonthAndDayOfWeek.java @@ -0,0 +1,36 @@ +package oncall.domain; + +import java.time.DayOfWeek; +import java.time.Month; +import java.time.format.TextStyle; +import java.util.Locale; + +public class MonthAndDayOfWeek { + private final Month month; + private final DayOfWeek dayOfWeek; + + public MonthAndDayOfWeek(Month month, DayOfWeek dayOfWeek) { + this.month = month; + this.dayOfWeek = dayOfWeek; + } + + public int getMonthNumber(){ + return month.getValue(); + } + + public int getLastDay() { + return month.minLength(); + } + + public int getDayOfWeekNumber() { + return dayOfWeek.getValue(); + } + + public String getDayOfWeekName() { + return dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN); + } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } +} diff --git a/src/main/java/oncall/domain/MonthlyRoster.java b/src/main/java/oncall/domain/MonthlyRoster.java new file mode 100644 index 00000000..cb01b04d --- /dev/null +++ b/src/main/java/oncall/domain/MonthlyRoster.java @@ -0,0 +1,30 @@ +package oncall.domain; + +public class MonthlyRoster { + private final int month; + private final int day; + private final String dayOfWeek; + private final String name; + private final boolean holiday; + + public MonthlyRoster(int month, int day, String dayOfWeek, String name, boolean holiday) { + this.month = month; + this.day = day; + this.dayOfWeek = dayOfWeek; + this.name = name; + this.holiday = holiday; + } + + @Override + public String toString() { + return String.valueOf(month) + "월 " + String.valueOf(day) + "일 " + dayOfWeek + isHoliday() + name; + } + + private String isHoliday() { + //주말은 아님 + if (holiday && !(dayOfWeek.equals("토") || dayOfWeek.equals("일"))) { + return "(휴일) "; + } + return " "; + } +} diff --git a/src/main/java/oncall/domain/Roster.java b/src/main/java/oncall/domain/Roster.java new file mode 100644 index 00000000..f24c8879 --- /dev/null +++ b/src/main/java/oncall/domain/Roster.java @@ -0,0 +1,52 @@ +package oncall.domain; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import oncall.exception.IllegalInputException; + +public class Roster { + private final List workers; + private static final int WORKERS_MIN_SIZE = 5; + private static final int WORKERS_MAX_SIZE = 35; + + + public Roster(List workers) { + validate(workers); + this.workers = workers; + } + + private void validate(List workers) { + if (workers.size() < WORKERS_MIN_SIZE || workers.size() > WORKERS_MAX_SIZE) { + throw new IllegalInputException(); + } + checkDuplicate(workers); + } + + private void checkDuplicate(List workers) { + Set names = new HashSet<>(); + for (Worker w : workers) { + names.add(w.getName()); + } + if (workers.size() != names.size()) { + throw new IllegalInputException(); + } + } + + public List getWorkers() { + return workers; + } + + public String getWorkerNameByIndex(int i) { + return workers.get(i).getName(); + } + + public void swapRoster(int i) { + Collections.swap(workers, i, i + 1); + } + + public int getRosterSize() { + return workers.size(); + } +} diff --git a/src/main/java/oncall/domain/Worker.java b/src/main/java/oncall/domain/Worker.java new file mode 100644 index 00000000..9d9d4c89 --- /dev/null +++ b/src/main/java/oncall/domain/Worker.java @@ -0,0 +1,24 @@ +package oncall.domain; + +import oncall.exception.IllegalNameException; + +public class Worker { + private static final int NAME_LENGTH = 5; + + private final String name; + + public Worker(String name){ + validate(name); + this.name = name; + } + + private void validate(String name) { + if(name.length() > NAME_LENGTH){ + throw new IllegalNameException(); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/oncall/exception/CustomException.java b/src/main/java/oncall/exception/CustomException.java new file mode 100644 index 00000000..5b09cae2 --- /dev/null +++ b/src/main/java/oncall/exception/CustomException.java @@ -0,0 +1,9 @@ +package oncall.exception; + +public class CustomException extends IllegalArgumentException{ + private static final String ERROR_PREFIX = "[ERROR] "; + + CustomException(String message){ + super(ERROR_PREFIX + message); + } +} diff --git a/src/main/java/oncall/exception/IllegalInputException.java b/src/main/java/oncall/exception/IllegalInputException.java new file mode 100644 index 00000000..9efc7f4a --- /dev/null +++ b/src/main/java/oncall/exception/IllegalInputException.java @@ -0,0 +1,7 @@ +package oncall.exception; + +public class IllegalInputException extends CustomException { + public IllegalInputException() { + super("유효하지 않은 입력 값입니다. 다시 입력해 주세요."); + } +} diff --git a/src/main/java/oncall/exception/IllegalNameException.java b/src/main/java/oncall/exception/IllegalNameException.java new file mode 100644 index 00000000..8e64b237 --- /dev/null +++ b/src/main/java/oncall/exception/IllegalNameException.java @@ -0,0 +1,7 @@ +package oncall.exception; + +public class IllegalNameException extends CustomException { + public IllegalNameException() { + super("이름이 5자 초과입니다. 다시 입력해 주세요."); + } +} diff --git a/src/main/java/oncall/service/CustomService.java b/src/main/java/oncall/service/CustomService.java new file mode 100644 index 00000000..6f42514d --- /dev/null +++ b/src/main/java/oncall/service/CustomService.java @@ -0,0 +1,81 @@ +package oncall.service; + +import java.time.DayOfWeek; +import java.time.Month; +import java.time.format.TextStyle; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import oncall.config.Holidays; +import oncall.domain.CombinedRoster; +import oncall.domain.MonthAndDayOfWeek; +import oncall.domain.MonthlyRoster; +import oncall.domain.Roster; + +public class CustomService { + public List makeMonthlyRoster(MonthAndDayOfWeek monthAndDayOfWeek, CombinedRoster combinedRoster) { + int monthNumber = monthAndDayOfWeek.getMonthNumber(); // 5 + int lastDay = monthAndDayOfWeek.getLastDay(); // 31 + + Roster weekdayRoster = combinedRoster.getWeekdayRoster(); + Roster weekendRoster = combinedRoster.getWeekendRoster(); + + int dayOfWeekIndex = monthAndDayOfWeek.getDayOfWeekNumber(); // 월-1 , 일-7 + List monthlyRosters = new ArrayList<>(); + + int weekdayIndex = 0; + int weekendIndex = 0; + String name = ""; + for (int day = 1; day <= lastDay; day++) { + boolean isHoliday = isHoliday(monthNumber, day, dayOfWeekIndex); + if (isHoliday) { + if(name.equals(weekendRoster.getWorkerNameByIndex(weekendIndex))){//이전 근무자와 이름이 연속되는지 확인 + weekendRoster.swapRoster(weekendIndex);//연속되면 스왑 + } + name = weekendRoster.getWorkerNameByIndex(weekendIndex); + weekendIndex += 1; + if(weekendIndex > weekendRoster.getRosterSize() - 1){ //순번 다 돌면 초기화 + weekendIndex = 0; + } + } + if (!isHoliday) { + if(name.equals(weekdayRoster.getWorkerNameByIndex(weekdayIndex))){//이전 근무자와 이름이 연속되는지 확인 + weekdayRoster.swapRoster(weekdayIndex);//연속되면 스왑 + } + name = weekdayRoster.getWorkerNameByIndex(weekdayIndex); + weekdayIndex += 1; + if(weekdayIndex > weekdayRoster.getRosterSize() - 1){ //순번 다 돌면 초기화 + weekdayIndex = 0; + } + } + //리스트에 추가 + monthlyRosters.add(new MonthlyRoster(monthNumber, day, getDayOfWeekName(dayOfWeekIndex), name, isHoliday)); + + //요일 순번 다 돌면 초기화 + dayOfWeekIndex += 1; + if (dayOfWeekIndex > 7) { + dayOfWeekIndex = 1; + } + } + return monthlyRosters; + } + + private String getDayOfWeekName(int dayOfWeekNumber) { + DayOfWeek dayOfWeek = DayOfWeek.of(dayOfWeekNumber); + return dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN); + } + + private boolean isHoliday(int month, int day, int dayOfWeekNumber) { + for (Holidays h : Holidays.values()) { + if (h.getMonth() == month && h.getDay() == day) { + return true; + } + } + if (DayOfWeek.of(dayOfWeekNumber) == DayOfWeek.SATURDAY || DayOfWeek.of(dayOfWeekNumber) == DayOfWeek.SUNDAY) { + return true; + } + + return false; + } +} diff --git a/src/main/java/oncall/view/InputView.java b/src/main/java/oncall/view/InputView.java new file mode 100644 index 00000000..5ef8ccdc --- /dev/null +++ b/src/main/java/oncall/view/InputView.java @@ -0,0 +1,68 @@ +package oncall.view; + +import camp.nextstep.edu.missionutils.Console; +import java.time.DateTimeException; +import java.time.DayOfWeek; +import java.time.Month; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import oncall.domain.MonthAndDayOfWeek; +import oncall.domain.Roster; +import oncall.domain.Worker; +import oncall.exception.IllegalInputException; + +public class InputView { + private static final String DELIMITER = ","; + + public MonthAndDayOfWeek getMonthDayOfWeek() { + String input = Console.readLine(); + String[] splitInput = input.split(DELIMITER); + + Month month = parseMonth(splitInput[0]); + DayOfWeek dayOfWeek = parseDayOfWeek(splitInput[1]); + + return new MonthAndDayOfWeek(month, dayOfWeek); + } + + private Month parseMonth(String s) { + try { + return Month.of(Integer.parseInt(s)); + } catch (NumberFormatException | DateTimeException e) { + throw new IllegalInputException(); + } + } + + private DayOfWeek parseDayOfWeek(String s) { + try { + Map DayOfWeeks = new HashMap<>(); + DayOfWeeks.put("월", 1); + DayOfWeeks.put("화", 2); + DayOfWeeks.put("수", 3); + DayOfWeeks.put("목", 4); + DayOfWeeks.put("금", 5); + DayOfWeeks.put("토", 6); + DayOfWeeks.put("일", 7); + + int index = DayOfWeeks.get(s); + return DayOfWeek.of(index); + } catch (DateTimeException | NullPointerException e) { + throw new IllegalInputException(); + } + } + + public Roster getRoster() { + String input = Console.readLine(); + String[] names = input.split(DELIMITER); + + List workers = new ArrayList<>(); + for(String name : names) { + workers.add(new Worker(name)); + } + return new Roster(workers); + } + +} diff --git a/src/main/java/oncall/view/OutputView.java b/src/main/java/oncall/view/OutputView.java new file mode 100644 index 00000000..9b9b2e13 --- /dev/null +++ b/src/main/java/oncall/view/OutputView.java @@ -0,0 +1,24 @@ +package oncall.view; + +import java.util.List; +import oncall.domain.MonthlyRoster; + +public class OutputView { + public void printGetMonthDayOfWeek() { + System.out.print("비상 근무를 배정할 월과 시작 요일을 입력하세요> "); + } + + public void printGetWeekdayRoster() { + System.out.print("평일 비상 근무 순번대로 사원 닉네임을 입력하세요> "); + } + + public void printGetWeekendRoster() { + System.out.print("휴일 비상 근무 순번대로 사원 닉네임을 입력하세요> "); + } + + public void printMonthlyRoster(List monthlyRoster) { + for (MonthlyRoster roster : monthlyRoster) { + System.out.println(roster.toString()); + } + } +} diff --git a/src/test/java/oncall/ApplicationTest.java b/src/test/java/oncall/ApplicationTest.java index 79198bce..6b26393c 100644 --- a/src/test/java/oncall/ApplicationTest.java +++ b/src/test/java/oncall/ApplicationTest.java @@ -2,8 +2,12 @@ import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import camp.nextstep.edu.missionutils.test.NsTest; +import oncall.domain.Worker; +import oncall.exception.IllegalNameException; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class ApplicationTest extends NsTest { @@ -72,6 +76,15 @@ class ApplicationTest extends NsTest { }); } + @DisplayName("근무자의 이름은 5자 이하여야 한다.") + @Test + void WorkerTest() { + assertThatThrownBy(() -> new Worker("abcdef")) + .isInstanceOf(IllegalNameException.class) + .hasMessage("[ERROR] 이름이 5자 초과입니다. 다시 입력해 주세요."); + } + + @Override protected void runMain() { Application.main(new String[]{});