diff --git a/README.md b/README.md index 572a1800..824c6312 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,74 @@ # java-attendance-precourse -스크린샷 2025-11-27 20 49 53 -스크린샷 2025-11-27 20 49 55 -스크린샷 2025-11-27 20 49 56 -스크린샷 2025-11-27 20 49 57 -스크린샷 2025-11-27 20 49 58 -스크린샷 2025-11-27 20 50 05 -스크린샷 2025-11-27 20 50 06 -스크린샷 2025-11-27 20 50 08 -스크린샷 2025-11-27 20 50 09 -스크린샷 2025-11-27 20 50 10 -스크린샷 2025-11-27 20 50 11 -스크린샷 2025-11-27 20 51 49 +## 기능 구현 목록 +> 기능 작동 순서대로 작성 + +1. 파일 입력 및 출석 정보 저장 +2. 기능 선택 및 검증 + - 출력문 + - 날짜 요일 + - 예외 사항 + - 1,2,3,4,Q가 아닌 경우 -> Validator + - 잘못된 형식을 입력하였습니다. +3. 출석확인 + 1. 오늘이 등교일이 아니면 예외 발생 -> Service + - %s은 등교일이 아닙니다. + 2. 닉네임 입력 및 검증 + - 예외 사항 + - 등록되지 않은 닉네임인 경우 -> Crews + - 등록되지 않은 닉네임입니다. + - 이미 출석을 하였는데 다시 출석 확인을 하는 경우 -> Attendance + - 이미 출석을 확인하였습니다. 필요한 경우 수정 기능을 이용해주세요. + 3. 등교 시간 입력 및 검증 + - 예외 사항 + - 시간을 잘못된 형식으로 입력한 경우 -> Validator + - 잘못된 형식을 입력하였습니다. + - 등교 시간이 캠퍼스 운영 시간이 아닌 경우 -> Service + - 캠퍼스 운영 시간에만 출석이 가능합니다. + 4. 출석 정보 저장 + 5. 확인 결과 출력 + - 월, 일, 요일, 시간, 출석 상태 +4. 출석 수정 + 1. 닉네임 입력 및 검증 + - 예외 사항 + - 등록되지 않은 닉네임인 경우 -> Crews + - 등록되지 않은 닉네임입니다. + 2. 날짜 입력 및 검증 + - 예외 사항 + - 잘못된 형식을 입력한 경우 -> Validator + - 잘못된 형식을 입력하였습니다. + - 등교일이 아니면 예외 발생 -> Service + - %s은 등교일이 아닙니다. + - 미래 날짜로 출석을 수정하는 경우 -> Service + - 아직 수정할 수 없습니다. + 3. 시각 입력 및 검증 + - 예외 사항 + - 시간을 잘못된 형식으로 입력한 경우 -> Validator + - 잘못된 형식을 입력하였습니다. + - 등교 시간이 캠퍼스 운영 시간이 아닌 경우 -> Service + - 캠퍼스 운영 시간에만 출석이 가능합니다. + 4. 출석 정보 수정 + 5. 수정 결과 출력 + - 월, 일, 요일 + - 이전 시간, 이전 출석 상태 + - 변경 시간, 변경 출석 상태 +5. 크루별 출석 기록 확인 + 1. 닉네임 입력 및 검증 + - 예외 사항 + - 등록되지 않은 닉네임인 경우 -> Crews + - 등록되지 않은 닉네임입니다. + 2. 출석 기록 출력 + - 전날까지의 출석 기록 출력 + - 월, 일, 요일, 시간, 출석 상태 + - 아예 안온날은 시간을 --:-- 으로 출력 + - 출석, 지각, 결석 횟수 + - 위험 대상자라면 해당 상태 출력 +6. 제적 위험자 확인 + - 전날까지의 기록으로 제적 위험자 파악 + - 이름, 결석 횟수, 지각 횟수, 위험 상태 + - 정렬 기준 + 1. 결석 + 지각/3 내림 + 2. 결석 내림 + 3. 지각 내림 + 4. 이름 오름 +7. 종료 \ No newline at end of file diff --git a/src/main/java/attendance/Application.java b/src/main/java/attendance/Application.java index e9b866ea..1097af77 100644 --- a/src/main/java/attendance/Application.java +++ b/src/main/java/attendance/Application.java @@ -1,7 +1,19 @@ package attendance; +import attendance.command.MenuCommandRegistry; +import attendance.controller.AttendanceController; +import attendance.service.AttendanceService; +import java.io.IOException; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + AttendanceService service = new AttendanceService(); + MenuCommandRegistry registry = MenuCommandRegistry.from(service); + AttendanceController controller = new AttendanceController(registry, service); + try { + controller.run(); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/src/main/java/attendance/command/Command.java b/src/main/java/attendance/command/Command.java new file mode 100644 index 00000000..cee363fe --- /dev/null +++ b/src/main/java/attendance/command/Command.java @@ -0,0 +1,5 @@ +package attendance.command; + +public interface Command { + void execute(); +} diff --git a/src/main/java/attendance/command/MenuCommandRegistry.java b/src/main/java/attendance/command/MenuCommandRegistry.java new file mode 100644 index 00000000..d7b07b30 --- /dev/null +++ b/src/main/java/attendance/command/MenuCommandRegistry.java @@ -0,0 +1,30 @@ +package attendance.command; + +import attendance.command.impl.CheckCommand; +import attendance.command.impl.ModificationCommand; +import attendance.command.impl.RecordQueryCommand; +import attendance.command.impl.DangerQueryCommand; +import attendance.service.AttendanceService; +import java.util.EnumMap; + +public class MenuCommandRegistry { + + private final EnumMap commands; + + private MenuCommandRegistry(EnumMap commands) { + this.commands = commands; + } + + public static MenuCommandRegistry from(AttendanceService service) { + EnumMap map = new EnumMap<>(MenuOption.class); + map.put(MenuOption.A, new CheckCommand(service)); + map.put(MenuOption.B, new ModificationCommand(service)); + map.put(MenuOption.C, new RecordQueryCommand(service)); + map.put(MenuOption.D, new DangerQueryCommand(service)); + return new MenuCommandRegistry(map); + } + + public void execute(MenuOption option) { + commands.get(option).execute(); + } +} diff --git a/src/main/java/attendance/command/MenuOption.java b/src/main/java/attendance/command/MenuOption.java new file mode 100644 index 00000000..6e6a9bd5 --- /dev/null +++ b/src/main/java/attendance/command/MenuOption.java @@ -0,0 +1,25 @@ +package attendance.command; + +import attendance.constant.ErrorMessage; +import java.util.Arrays; + +public enum MenuOption { + A("1"), + B("2"), + C("3"), + D("4"), + QUIT("Q"); + + private final String command; + + MenuOption(String command) { + this.command = command; + } + + public static MenuOption from(String command) { + return Arrays.stream(values()) + .filter(opt -> opt.command.equals(command)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ErrorMessage.FORMAT_ERROR.getErrorMessage())); + } +} diff --git a/src/main/java/attendance/command/impl/CheckCommand.java b/src/main/java/attendance/command/impl/CheckCommand.java new file mode 100644 index 00000000..0de19ea1 --- /dev/null +++ b/src/main/java/attendance/command/impl/CheckCommand.java @@ -0,0 +1,35 @@ +package attendance.command.impl; + +import attendance.command.Command; +import attendance.service.AttendanceService; +import attendance.util.InputParser; +import attendance.view.InputView; +import attendance.view.OutputView; +import camp.nextstep.edu.missionutils.DateTimes; +import java.time.LocalDate; +import java.time.LocalTime; + +public class CheckCommand implements Command { + + private final AttendanceService service; + + public CheckCommand(AttendanceService service) { + this.service = service; + } + + @Override + public void execute() { + LocalDate now = DateTimes.now().toLocalDate(); + service.validateHoliday(now); + + String name = InputParser.parseName(InputView.readName()); + service.validateCheckPossible(name, now); + + LocalTime time = InputParser.parseTime(InputView.readTime()); + service.validateOperationTime(time); + + service.check(name, now, time); + + OutputView.printCheckResult(now, time); + } +} diff --git a/src/main/java/attendance/command/impl/DangerQueryCommand.java b/src/main/java/attendance/command/impl/DangerQueryCommand.java new file mode 100644 index 00000000..e33d5acd --- /dev/null +++ b/src/main/java/attendance/command/impl/DangerQueryCommand.java @@ -0,0 +1,23 @@ +package attendance.command.impl; + +import attendance.command.Command; +import attendance.domain.Crew; +import attendance.service.AttendanceService; +import attendance.view.OutputView; +import java.util.List; + +public class DangerQueryCommand implements Command { + + private final AttendanceService service; + + public DangerQueryCommand(AttendanceService service) { + this.service = service; + } + + @Override + public void execute() { + List dangers = service.getDangers(); + + OutputView.printDangers(dangers); + } +} diff --git a/src/main/java/attendance/command/impl/ModificationCommand.java b/src/main/java/attendance/command/impl/ModificationCommand.java new file mode 100644 index 00000000..c9aa4b8f --- /dev/null +++ b/src/main/java/attendance/command/impl/ModificationCommand.java @@ -0,0 +1,34 @@ +package attendance.command.impl; + +import attendance.command.Command; +import attendance.service.AttendanceService; +import attendance.util.InputParser; +import attendance.view.InputView; +import attendance.view.OutputView; +import java.time.LocalDate; +import java.time.LocalTime; + +public class ModificationCommand implements Command { + + private final AttendanceService service; + + public ModificationCommand(AttendanceService service) { + this.service = service; + } + + @Override + public void execute() { + String name = InputParser.parseName(InputView.readModifiedName()); + service.validateModificationPossible(name); + + LocalDate date = InputParser.parseDate(InputView.readModifiedDate()); + service.validateModificationPossible(date); + + LocalTime time = InputParser.parseTime(InputView.readModifiedTime()); + service.validateModificationPossible(time); + + LocalTime oldTime = service.modify(name, date, time); + + OutputView.printModificationResult(date, time, oldTime); + } +} diff --git a/src/main/java/attendance/command/impl/RecordQueryCommand.java b/src/main/java/attendance/command/impl/RecordQueryCommand.java new file mode 100644 index 00000000..e3b32eab --- /dev/null +++ b/src/main/java/attendance/command/impl/RecordQueryCommand.java @@ -0,0 +1,25 @@ +package attendance.command.impl; + +import attendance.command.Command; +import attendance.domain.Crew; +import attendance.service.AttendanceService; +import attendance.util.InputParser; +import attendance.view.InputView; +import attendance.view.OutputView; + +public class RecordQueryCommand implements Command { + + private final AttendanceService service; + + public RecordQueryCommand(AttendanceService service) { + this.service = service; + } + + @Override + public void execute() { + String name = InputParser.parseName(InputView.readName()); + Crew crew = service.getAttendanceRecords(name); + + OutputView.printRecords(crew); + } +} diff --git a/src/main/java/attendance/constant/AttendanceState.java b/src/main/java/attendance/constant/AttendanceState.java new file mode 100644 index 00000000..f1e7a663 --- /dev/null +++ b/src/main/java/attendance/constant/AttendanceState.java @@ -0,0 +1,34 @@ +package attendance.constant; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; + +public enum AttendanceState { + + ABSENCE(30, "결석"), + LATE(5, "지각"), + ATTENDANCE(0, "출석"), + ; + + private final int lateMinutes; + private final String name; + + AttendanceState(int lateMinutes, String name) { + this.lateMinutes = lateMinutes; + this.name = name; + } + + public static AttendanceState of(LocalDate date, LocalTime time) { + Standard standard = Standard.from(date); + + return Arrays.stream(values()) + .filter(danger -> standard.getStartTime().plusMinutes(danger.lateMinutes).isBefore(time)) + .findFirst() + .orElse(ATTENDANCE); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/attendance/constant/Danger.java b/src/main/java/attendance/constant/Danger.java new file mode 100644 index 00000000..2eb41649 --- /dev/null +++ b/src/main/java/attendance/constant/Danger.java @@ -0,0 +1,31 @@ +package attendance.constant; + +import java.util.Arrays; + +public enum Danger { + + OUT(6, "제적"), + INTERVIEW(3, "면담"), + WARNING(2, "경고"), + NONE(0, ""), + ; + + private final int absenceCount; + private final String name; + + Danger(int absenceCount, String name) { + this.absenceCount = absenceCount; + this.name = name; + } + + public static Danger from(int absenceCount) { + return Arrays.stream(values()) + .filter(danger -> danger.absenceCount <= absenceCount) + .findFirst() + .orElse(NONE); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/attendance/constant/ErrorMessage.java b/src/main/java/attendance/constant/ErrorMessage.java new file mode 100644 index 00000000..d47383ff --- /dev/null +++ b/src/main/java/attendance/constant/ErrorMessage.java @@ -0,0 +1,23 @@ +package attendance.constant; + +public enum ErrorMessage { + + FORMAT_ERROR("잘못된 형식을 입력하였습니다."), + NO_ATTENDANCE_DAY_ERROR("%s은 등교일이 아닙니다."), + NO_EXIST_NAME_ERROR("등록되지 않은 닉네임입니다."), + ALREADY_ATTENDANCE_ERROR("이미 출석을 확인하였습니다. 필요한 경우 수정 기능을 이용해주세요."), + NO_OPERATION_TIME_ERROR("캠퍼스 운영 시간에만 출석이 가능합니다."), + FUTURE_DATE_ERROR("아직 수정할 수 없습니다."), + ; + + private static final String ERROR_MESSAGE_PREFIX = "[ERROR] "; + private final String errorMessage; + + ErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorMessage(Object... args) { + return ERROR_MESSAGE_PREFIX + String.format(errorMessage, args); + } +} diff --git a/src/main/java/attendance/constant/Holiday.java b/src/main/java/attendance/constant/Holiday.java new file mode 100644 index 00000000..d1842cbb --- /dev/null +++ b/src/main/java/attendance/constant/Holiday.java @@ -0,0 +1,18 @@ +package attendance.constant; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +public enum Holiday { + + HOLI_DAY, + NONE; + + public static Holiday from(LocalDate date) { + if (date.getDayOfWeek().equals(DayOfWeek.SATURDAY) || date.getDayOfWeek().equals(DayOfWeek.SUNDAY) || date.isEqual(LocalDate.of(2024,12,25))) { + return HOLI_DAY; + } + + return NONE; + } +} diff --git a/src/main/java/attendance/constant/Standard.java b/src/main/java/attendance/constant/Standard.java new file mode 100644 index 00000000..0954bad9 --- /dev/null +++ b/src/main/java/attendance/constant/Standard.java @@ -0,0 +1,31 @@ +package attendance.constant; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; + +public enum Standard { + + MONDAY(LocalTime.of(13,0)), + OTHERS(LocalTime.of(10,0)), + ; + + private final LocalTime startTime; + + Standard(LocalTime startTime) { + this.startTime = startTime; + } + + public static Standard from(LocalDate date) { + + if (date.getDayOfWeek().equals(DayOfWeek.MONDAY) && Holiday.from(date).equals(Holiday.NONE)) { + return MONDAY; + } + + return OTHERS; + } + + public LocalTime getStartTime() { + return startTime; + } +} diff --git a/src/main/java/attendance/controller/AttendanceController.java b/src/main/java/attendance/controller/AttendanceController.java new file mode 100644 index 00000000..e941f79a --- /dev/null +++ b/src/main/java/attendance/controller/AttendanceController.java @@ -0,0 +1,45 @@ +package attendance.controller; + +import attendance.command.MenuCommandRegistry; +import attendance.command.MenuOption; +import attendance.service.AttendanceService; +import attendance.util.InputParser; +import attendance.util.file.FileReader; +import attendance.view.InputView; +import java.io.IOException; +import java.util.List; + +public class AttendanceController { + + private final MenuCommandRegistry registry; + private final AttendanceService service; + + public AttendanceController(MenuCommandRegistry registry, AttendanceService service) { + this.registry = registry; + this.service = service; + } + + public void run() throws IOException { + registerFileInfo(); + + while (true) { + MenuOption option = readOption(); + + if (option.equals(MenuOption.QUIT)) { + return; + } + + registry.execute(option); + } + } + + private void registerFileInfo() throws IOException { + FileReader fileReader = new FileReader("src/main/resources/attendances.csv"); + List readLines = fileReader.readLines(); + service.registerFileInfo(readLines); + } + + private MenuOption readOption() { + return InputParser.parseMenu(InputView.readMenuSelection()); + } +} diff --git a/src/main/java/attendance/domain/Attendance.java b/src/main/java/attendance/domain/Attendance.java new file mode 100644 index 00000000..5df77ef9 --- /dev/null +++ b/src/main/java/attendance/domain/Attendance.java @@ -0,0 +1,87 @@ +package attendance.domain; + +import attendance.constant.AttendanceState; +import attendance.constant.Holiday; +import camp.nextstep.edu.missionutils.DateTimes; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; + +public class Attendance { + + private final Map attendances; + + public Attendance(Map attendances) { + this.attendances = attendances; + } + + public static Attendance from(Map dateTimes) { + Map attendances = new HashMap<>(); + + LocalDate now = DateTimes.now().toLocalDate(); + for (LocalDate date = LocalDate.of(2024,12,1); date.isBefore(now); date = date.plusDays(1)) { + if (isHoliDay(date)) { + continue; + } + + if (dateTimes.containsKey(date)) { + attendances.put(date, dateTimes.get(date)); + continue; + } + + attendances.put(date, LocalTime.of(23,59)); + } + + return new Attendance(attendances); + } + + private static boolean isHoliDay(LocalDate date) { + return !Holiday.from(date).equals(Holiday.NONE); + } + + public boolean contains(LocalDate date) { + return attendances.containsKey(date); + } + + public void check(LocalDate date, LocalTime time) { + attendances.put(date, time); + } + + public LocalTime modify(LocalDate date, LocalTime newTime) { + LocalTime oldTime = attendances.get(date); + attendances.put(date, newTime); + return oldTime; + } + + public Map getRecords() { + Map records = new HashMap<>(attendances); + records.remove(DateTimes.now().toLocalDate()); + + return records; + } + + public int getAttendanceCount() { + Map records = getRecords(); + + return (int) records.keySet().stream() + .filter(date -> AttendanceState.of(date, records.get(date)).equals(AttendanceState.ATTENDANCE)) + .count(); + } + + public int getLateCount() { + Map records = getRecords(); + + return (int) records.keySet().stream() + .filter(date -> AttendanceState.of(date, records.get(date)).equals(AttendanceState.LATE)) + .count(); + } + + public int getAbsenceCount() { + Map records = getRecords(); + + return (int) records.keySet().stream() + .filter(date -> AttendanceState.of(date, records.get(date)).equals(AttendanceState.ABSENCE)) + .count(); + } +} diff --git a/src/main/java/attendance/domain/Crew.java b/src/main/java/attendance/domain/Crew.java new file mode 100644 index 00000000..98f30dff --- /dev/null +++ b/src/main/java/attendance/domain/Crew.java @@ -0,0 +1,64 @@ +package attendance.domain; + +import attendance.constant.Danger; +import attendance.constant.ErrorMessage; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Map; + +public class Crew { + + private final String name; + private final Attendance attendance; + + private Crew(String name, Attendance attendance) { + this.name = name; + this.attendance = attendance; + } + + public static Crew of(String name, Map localDateTimes) { + return new Crew(name, Attendance.from(localDateTimes)); + } + + public String getName() { + return name; + } + + public void validateCheckPossible(LocalDate date) { + if (attendance.contains(date)) { + throw new IllegalArgumentException(ErrorMessage.ALREADY_ATTENDANCE_ERROR.getErrorMessage()); + } + } + + public void check(LocalDate date, LocalTime time) { + attendance.check(date, time); + } + + public LocalTime modify(LocalDate date, LocalTime newTime) { + return attendance.modify(date, newTime); + } + + public Map getAttendanceRecords() { + return attendance.getRecords(); + } + + public int getAttendanceCount() { + return attendance.getAttendanceCount(); + } + + public int getLateCount() { + return attendance.getLateCount(); + } + + public int getAbsenceCount() { + return attendance.getAbsenceCount(); + } + + public Danger getDangerState() { + return Danger.from(getAbsenceLateCount()); + } + + public int getAbsenceLateCount() { + return getAbsenceCount() + getLateCount() / 3; + } +} diff --git a/src/main/java/attendance/domain/Crews.java b/src/main/java/attendance/domain/Crews.java new file mode 100644 index 00000000..ad1bb3af --- /dev/null +++ b/src/main/java/attendance/domain/Crews.java @@ -0,0 +1,40 @@ +package attendance.domain; + +import attendance.constant.Danger; +import attendance.constant.ErrorMessage; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class Crews { + + private final List crews; + + private Crews(List crews) { + this.crews = crews; + } + + public static Crews from(Map> attendances) { + List crews = new ArrayList<>(); + for (String name : attendances.keySet()) { + Crew crew = Crew.of(name, attendances.get(name)); + crews.add(crew); + } + return new Crews(crews); + } + + public Crew getCrew(String name) { + return crews.stream() + .filter(crew -> crew.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ErrorMessage.NO_EXIST_NAME_ERROR.getErrorMessage())); + } + + public List getDangers() { + return new ArrayList<>(crews.stream() + .filter(crew -> !crew.getDangerState().equals(Danger.NONE)) + .toList()); + } +} diff --git a/src/main/java/attendance/service/AttendanceService.java b/src/main/java/attendance/service/AttendanceService.java new file mode 100644 index 00000000..3d32aeac --- /dev/null +++ b/src/main/java/attendance/service/AttendanceService.java @@ -0,0 +1,109 @@ +package attendance.service; + +import static java.util.Locale.KOREA; + +import attendance.constant.ErrorMessage; +import attendance.constant.Holiday; +import attendance.domain.Crew; +import attendance.domain.Crews; +import camp.nextstep.edu.missionutils.DateTimes; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AttendanceService { + + private static final LocalTime START_TIME = LocalTime.of(8, 0); + private static final LocalTime END_TIME = LocalTime.of(23, 0); + private static final DateTimeFormatter DATETIME_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", KOREA); + private static final DateTimeFormatter DATE_FMT = + DateTimeFormatter.ofPattern("M월 dd일 E요일", KOREA); + + private Crews crews; + + public void registerFileInfo(List readLines) { + Map> attendances = new HashMap<>(); + + readLines.removeFirst(); + for (String readLine : readLines) { + String[] split = readLine.split(","); + String name = split[0]; + LocalDateTime dateTime = LocalDateTime.parse(split[1], DATETIME_FMT); + LocalDate date = dateTime.toLocalDate(); + LocalTime time = dateTime.toLocalTime(); + + if (attendances.containsKey(name)) { + attendances.get(name).put(date, time); + continue; + } + + attendances.put(name, new HashMap<>(Map.of(date, time))); + } + + crews = Crews.from(attendances); + } + + public void validateHoliday(LocalDate date) { + if (isHoliday(date)) { + throw new IllegalArgumentException( + ErrorMessage.NO_ATTENDANCE_DAY_ERROR.getErrorMessage(date.format(DATE_FMT))); + } + } + + private static boolean isHoliday(LocalDate date) { + return !Holiday.from(date).equals(Holiday.NONE); + } + + public void validateCheckPossible(String name, LocalDate date) { + Crew crew = crews.getCrew(name); + crew.validateCheckPossible(date); + } + + public void check(String name, LocalDate date, LocalTime time) { + Crew crew = crews.getCrew(name); + crew.check(date, time); + } + + public void validateOperationTime(LocalTime time) { + if (time.isBefore(START_TIME) || time.isAfter(END_TIME)) { + throw new IllegalArgumentException(ErrorMessage.NO_OPERATION_TIME_ERROR.getErrorMessage()); + } + } + + public void validateModificationPossible(String name) { + crews.getCrew(name); + } + + public void validateModificationPossible(LocalDate date) { + validateHoliday(date); + validateFutureDate(date); + } + + private static void validateFutureDate(LocalDate date) { + if (date.isAfter(DateTimes.now().toLocalDate())) { + throw new IllegalArgumentException(ErrorMessage.FUTURE_DATE_ERROR.getErrorMessage()); + } + } + + public void validateModificationPossible(LocalTime time) { + validateOperationTime(time); + } + + public LocalTime modify(String name, LocalDate date, LocalTime newTime) { + Crew crew = crews.getCrew(name); + return crew.modify(date, newTime); + } + + public Crew getAttendanceRecords(String name) { + return crews.getCrew(name); + } + + public List getDangers() { + return crews.getDangers(); + } +} diff --git a/src/main/java/attendance/util/InputParser.java b/src/main/java/attendance/util/InputParser.java new file mode 100644 index 00000000..10b953bc --- /dev/null +++ b/src/main/java/attendance/util/InputParser.java @@ -0,0 +1,45 @@ +package attendance.util; + +import static java.util.Locale.KOREA; + +import attendance.command.MenuOption; +import camp.nextstep.edu.missionutils.DateTimes; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public final class InputParser { + + private static final DateTimeFormatter TIME_FMT = + DateTimeFormatter.ofPattern("HH:mm", KOREA); + + private InputParser() { + } + + public static String parseName(String rawInput) { + return rawInput.strip(); + } + + public static LocalDate parseDate(String rawInput) { + rawInput = rawInput.strip(); + + Validator.validateDayFormat(rawInput); + + int day = NumberConvertor.convertToNumber(rawInput); + + LocalDate now = DateTimes.now().toLocalDate(); + return LocalDate.of(now.getYear(), now.getMonthValue(), day); + } + + public static LocalTime parseTime(String rawInput) { + rawInput = rawInput.strip(); + + Validator.validateTimeFormat(rawInput); + + return LocalTime.parse(rawInput, TIME_FMT); + } + + public static MenuOption parseMenu(String rawInput) { + return MenuOption.from(rawInput.strip()); + } +} diff --git a/src/main/java/attendance/util/NumberConvertor.java b/src/main/java/attendance/util/NumberConvertor.java new file mode 100644 index 00000000..b68a85f7 --- /dev/null +++ b/src/main/java/attendance/util/NumberConvertor.java @@ -0,0 +1,14 @@ +package attendance.util; + +import attendance.constant.ErrorMessage; + +public final class NumberConvertor { + + public static int convertToNumber(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + } +} diff --git a/src/main/java/attendance/util/Validator.java b/src/main/java/attendance/util/Validator.java new file mode 100644 index 00000000..2da3ff69 --- /dev/null +++ b/src/main/java/attendance/util/Validator.java @@ -0,0 +1,23 @@ +package attendance.util; + +import attendance.constant.ErrorMessage; + +public final class Validator { + + private static final String DAY_FORMAT = "^[1-9]|[1-2]\\d|3[0-1]$"; + private static final String TIME_FORMAT = "([01]\\d|2[0-3]):[0-5]\\d"; + + private Validator() {} + + public static void validateDayFormat(String rawInput) { + if (!rawInput.matches(DAY_FORMAT)) { + throw new IllegalArgumentException(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + } + + public static void validateTimeFormat(String rawInput) { + if (!rawInput.matches(TIME_FORMAT)) { + throw new IllegalArgumentException(ErrorMessage.FORMAT_ERROR.getErrorMessage()); + } + } +} diff --git a/src/main/java/attendance/util/file/FileReader.java b/src/main/java/attendance/util/file/FileReader.java new file mode 100644 index 00000000..621174fb --- /dev/null +++ b/src/main/java/attendance/util/file/FileReader.java @@ -0,0 +1,32 @@ +package attendance.util.file; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FileReader { + + private static final int BUFFER_SIZE = 8192; + + private final java.io.FileReader fr; + + public FileReader(String fileName) throws IOException { + fr = new java.io.FileReader(fileName, UTF_8); + } + + public List readLines() throws IOException { + List contents = new ArrayList<>(); + BufferedReader br = new BufferedReader(fr, BUFFER_SIZE); + + String line; + while ((line = br.readLine()) != null) { + contents.add(line); + } + br.close(); + + return new ArrayList<>(contents); + } +} diff --git a/src/main/java/attendance/view/InputView.java b/src/main/java/attendance/view/InputView.java new file mode 100644 index 00000000..9793b513 --- /dev/null +++ b/src/main/java/attendance/view/InputView.java @@ -0,0 +1,48 @@ +package attendance.view; + +import static java.util.Locale.KOREA; + +import camp.nextstep.edu.missionutils.Console; +import camp.nextstep.edu.missionutils.DateTimes; +import java.time.format.DateTimeFormatter; + +public class InputView { + + private static final DateTimeFormatter DATE_FMT = + DateTimeFormatter.ofPattern("M월 dd일 E요일", KOREA); + + public static String readMenuSelection() { + System.out.printf("오늘은 %s입니다. 기능을 선택해 주세요.\n" + + "1. 출석 확인\n" + + "2. 출석 수정\n" + + "3. 크루별 출석 기록 확인\n" + + "4. 제적 위험자 확인\n" + + "Q. 종료\n", DateTimes.now().toLocalDate().format(DATE_FMT)); + return Console.readLine(); + } + + public static String readName() { + System.out.println("닉네임을 입력해 주세요."); + return Console.readLine(); + } + + public static String readTime() { + System.out.println("등교 시간을 입력해 주세요."); + return Console.readLine(); + } + + public static String readModifiedName() { + System.out.println("출석을 수정하려는 크루의 닉네임을 입력해 주세요."); + return Console.readLine(); + } + + public static String readModifiedDate() { + System.out.println("수정하려는 날짜(일)를 입력해 주세요."); + return Console.readLine(); + } + + public static String readModifiedTime() { + System.out.println("언제로 변경하겠습니까?"); + return Console.readLine(); + } +} diff --git a/src/main/java/attendance/view/OutputView.java b/src/main/java/attendance/view/OutputView.java new file mode 100644 index 00000000..2a43a21d --- /dev/null +++ b/src/main/java/attendance/view/OutputView.java @@ -0,0 +1,86 @@ +package attendance.view; + +import static java.util.Locale.KOREA; + +import attendance.constant.AttendanceState; +import attendance.constant.Danger; +import attendance.domain.Crew; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +public class OutputView { + + private static final DateTimeFormatter DATETIME_FMT = + DateTimeFormatter.ofPattern("M월 dd일 E요일 HH:mm", KOREA); + private static final DateTimeFormatter DATE_FMT = + DateTimeFormatter.ofPattern("M월 dd일 E요일", KOREA); + private static final DateTimeFormatter TIME_FMT = + DateTimeFormatter.ofPattern("HH:mm", KOREA); + + private OutputView() { + } + + public static void printCheckResult(LocalDate date, LocalTime time) { + System.out.printf("%s (%s)\n\n", LocalDateTime.of(date, time).format(DATETIME_FMT), + AttendanceState.of(date, time).getName()); + } + + public static void printModificationResult(LocalDate date, LocalTime time, LocalTime oldTime) { + if (oldTime.isAfter(LocalTime.of(23, 58))) { + System.out.printf("%s --:-- (결석) -> %s (%s) 수정 완료!\n\n", date.format(DATE_FMT), + time.format(TIME_FMT), AttendanceState.of(date, time).getName()); + return; + } + System.out.printf("%s %s (%s) -> %s (%s) 수정 완료!\n\n", date.format(DATE_FMT), + oldTime.format(TIME_FMT), AttendanceState.of(date, oldTime).getName(), + time.format(TIME_FMT), AttendanceState.of(date, time).getName()); + } + + public static void printRecords(Crew crew) { + System.out.printf("이번 달 %s의 출석 기록입니다.\n\n", crew.getName()); + + Map attendanceRecords = crew.getAttendanceRecords(); + List dates = new ArrayList<>(attendanceRecords.keySet()); + dates.sort(null); + for (LocalDate date : dates) { + LocalTime time = attendanceRecords.get(date); + if (time.isAfter(LocalTime.of(23, 58))) { + System.out.printf("%s --:-- (%s)\n", date.format(DATE_FMT), AttendanceState.of(date, time).getName()); + continue; + } + System.out.printf("%s %s (%s)\n", date.format(DATE_FMT), time.format(TIME_FMT), + AttendanceState.of(date, time).getName()); + } + System.out.println(); + + System.out.printf("출석: %d회\n", crew.getAttendanceCount()); + System.out.printf("지각: %d회\n", crew.getLateCount()); + System.out.printf("결석: %d회\n\n", crew.getAbsenceCount()); + + Danger dangerState = crew.getDangerState(); + if (!dangerState.equals(Danger.NONE)) { + System.out.printf("%s 대상자입니다.\n", dangerState.getName()); + } + System.out.println(); + } + + public static void printDangers(List dangers) { + dangers.sort(Comparator.comparingInt(Crew::getAbsenceLateCount).reversed() + .thenComparingInt(Crew::getAbsenceCount).reversed() + .thenComparingInt(Crew::getLateCount).reversed() + .thenComparing(Crew::getName)); + + System.out.println("제적 위험자 조회 결과"); + for (Crew danger : dangers) { + System.out.printf("- %s: 결석 %d회, 지각 %d회 (%s)\n", + danger.getName(), danger.getAbsenceCount(), danger.getLateCount(), danger.getDangerState().getName()); + } + System.out.println(); + } +} diff --git a/src/test/java/attendance/ApplicationTest.java b/src/test/java/attendance/ApplicationTest.java index c1b43494..9027808f 100644 --- a/src/test/java/attendance/ApplicationTest.java +++ b/src/test/java/attendance/ApplicationTest.java @@ -1,14 +1,13 @@ package attendance; -import camp.nextstep.edu.missionutils.test.NsTest; -import org.junit.jupiter.api.Test; - -import java.time.LocalDate; - import static camp.nextstep.edu.missionutils.test.Assertions.assertNowTest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import camp.nextstep.edu.missionutils.test.NsTest; +import java.time.LocalDate; +import org.junit.jupiter.api.Test; + class ApplicationTest extends NsTest { @Test void 잘못된_형식_예외_테스트() { @@ -77,6 +76,54 @@ class ApplicationTest extends NsTest { ); } + @Test + void 제적_위험자_조회_기능_테스트_12월_12일() { + assertNowTest( + () -> { + run("4", "Q"); + assertThat(output()).contains( + "제적 위험자 조회 결과\n" + + "- 빙티: 결석 2회, 지각 3회 (면담)\n" + + "- 이든: 결석 2회, 지각 3회 (면담)" + ); + }, + LocalDate.of(2024, 12, 12).atStartOfDay() + ); + } + + @Test + void 제적_위험자_조회_기능_테스트_12월_13일() { + assertNowTest( + () -> { + run("4", "Q"); + assertThat(output()).contains( + "- 빙티: 결석 3회, 지각 3회 (면담)\n" + + "- 이든: 결석 2회, 지각 4회 (면담)\n" + + "- 빙봉: 결석 1회, 지각 5회 (경고)\n" + + "- 쿠키: 결석 2회, 지각 2회 (경고)" + ); + }, + LocalDate.of(2024, 12, 13).atStartOfDay() + ); + } + + @Test + void 제적_위험자_조회_기능_테스트_12월_14일() { + assertNowTest( + () -> { + run("4", "Q"); + assertThat(output()).contains( + "- 빙티: 결석 3회, 지각 4회 (면담)\n" + + "- 빙봉: 결석 1회, 지각 6회 (면담)\n" + + "- 이든: 결석 2회, 지각 5회 (면담)\n" + + "- 쿠키: 결석 2회, 지각 3회 (면담)\n" + + "- 짱수: 결석 2회, 지각 0회 (경고)" + ); + }, + LocalDate.of(2024, 12, 14).atStartOfDay() + ); + } + @Override protected void runMain() { Application.main(new String[]{});