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
-
-
-
-
-
-
-
-
-
-
-
-
+## 기능 구현 목록
+> 기능 작동 순서대로 작성
+
+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[]{});