diff --git a/src/main/java/turing/turing/domain/exam/ExamController.java b/src/main/java/turing/turing/domain/exam/ExamController.java index 6ed5ee9..bff2d61 100644 --- a/src/main/java/turing/turing/domain/exam/ExamController.java +++ b/src/main/java/turing/turing/domain/exam/ExamController.java @@ -22,7 +22,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/exam") +@RequestMapping("/api/exam") public class ExamController { private final ExamService examService; diff --git a/src/main/java/turing/turing/domain/exam/dto/CreateExamRequest.java b/src/main/java/turing/turing/domain/exam/dto/CreateExamRequest.java index e57a20a..773b3dd 100644 --- a/src/main/java/turing/turing/domain/exam/dto/CreateExamRequest.java +++ b/src/main/java/turing/turing/domain/exam/dto/CreateExamRequest.java @@ -1,20 +1,20 @@ package turing.turing.domain.exam.dto; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.LocalDate; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import turing.turing.global.util.validation.dateRange.ValidDateRange; +import turing.turing.global.util.validation.range.ValidRange; @Getter @NoArgsConstructor -@ValidDateRange +@ValidRange(startField = "startDate", endField = "endDate", type = LocalDate.class, message = "종료일은 시작일과 같거나 미래여야 합니다.") public class CreateExamRequest { - @NotEmpty(message = "시험명은 필수입니다.") + @NotBlank(message = "시험명은 필수입니다.") private String examName; @NotNull(message = "시작일은 필수입니다.") diff --git a/src/main/java/turing/turing/domain/exam/dto/UpdateExamRequest.java b/src/main/java/turing/turing/domain/exam/dto/UpdateExamRequest.java index 53cdef8..2e8a5d2 100644 --- a/src/main/java/turing/turing/domain/exam/dto/UpdateExamRequest.java +++ b/src/main/java/turing/turing/domain/exam/dto/UpdateExamRequest.java @@ -1,22 +1,24 @@ package turing.turing.domain.exam.dto; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.LocalDate; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import turing.turing.global.util.validation.range.ValidRange; @Getter @NoArgsConstructor +@ValidRange(startField = "startDate", endField = "endDate", type = LocalDate.class, message = "종료일은 시작일과 같거나 미래여야 합니다.") public class UpdateExamRequest { @NotNull(message = "시험Id는 필수입니다.") @Positive(message = "시험Id는 양수여야 합니다.") private Long examId; - @NotEmpty(message = "시험명은 필수입니다.") + @NotBlank(message = "시험명은 필수입니다.") private String examName; @NotNull(message = "시작일은 필수입니다.") diff --git a/src/main/java/turing/turing/domain/homework/Homework.java b/src/main/java/turing/turing/domain/homework/Homework.java index 45f9ff4..d29f1ff 100644 --- a/src/main/java/turing/turing/domain/homework/Homework.java +++ b/src/main/java/turing/turing/domain/homework/Homework.java @@ -17,7 +17,7 @@ import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; import turing.turing.domain.BaseEntity; -import turing.turing.domain.homework.dto.DetailedHomeworkDto; +import turing.turing.domain.homework.dto.UpdateHomeworkRequest; import turing.turing.domain.notebook.Notebook; @Getter @@ -74,7 +74,7 @@ public class Homework extends BaseEntity { private Boolean isDone; @Builder - public Homework(String category, String title, String rangeType, int rangeStart, int rangeEnd, String content, String memo, Notebook notebook, Boolean isDone) { + private Homework(String category, String title, String rangeType, int rangeStart, int rangeEnd, String content, String memo, Notebook notebook) { super(); this.category = category; this.title = title; @@ -87,7 +87,7 @@ public Homework(String category, String title, String rangeType, int rangeStart, this.isDone = false; } - public Long update(DetailedHomeworkDto request) { + public Long update(UpdateHomeworkRequest request) { this.category = request.getCategory(); this.title = request.getTitle(); this.rangeType = request.getRangeType(); @@ -99,8 +99,8 @@ public Long update(DetailedHomeworkDto request) { return request.getHomeworkId(); } - public Long updateDone(Boolean newDone) { - this.isDone = newDone; + public Long updateDone(Boolean doneStatus) { + this.isDone = doneStatus; return this.id; } diff --git a/src/main/java/turing/turing/domain/homework/HomeworkController.java b/src/main/java/turing/turing/domain/homework/HomeworkController.java index 1b97940..db89b53 100644 --- a/src/main/java/turing/turing/domain/homework/HomeworkController.java +++ b/src/main/java/turing/turing/domain/homework/HomeworkController.java @@ -1,5 +1,6 @@ package turing.turing.domain.homework; +import jakarta.validation.Valid; import java.net.URI; import io.swagger.v3.oas.annotations.Operation; @@ -18,6 +19,7 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import turing.turing.domain.homework.dto.CreateHomeworkRequest; import turing.turing.domain.homework.dto.DetailedHomeworkDto; +import turing.turing.domain.homework.dto.UpdateHomeworkRequest; @Tag(name = "Homework", description = "숙제") @RestController @@ -36,7 +38,7 @@ public ResponseEntity getHomework(@PathVariable Long homewo @Operation(summary = "숙제 생성") @PostMapping("") - public ResponseEntity createHomework(@RequestBody CreateHomeworkRequest request) { + public ResponseEntity createHomework(@RequestBody @Valid CreateHomeworkRequest request) { Long savedId = homeworkService.createHomework(request); URI location = ServletUriComponentsBuilder.fromCurrentRequest() @@ -57,7 +59,7 @@ public ResponseEntity deleteHomework(@PathVariable Long homeworkId) { @Operation(summary = "숙제 수정") @PutMapping("") - public ResponseEntity updateHomework(@RequestBody DetailedHomeworkDto request) { + public ResponseEntity updateHomework(@RequestBody @Valid UpdateHomeworkRequest request) { return ResponseEntity.ok(homeworkService.updateHomework(request)); } diff --git a/src/main/java/turing/turing/domain/homework/HomeworkService.java b/src/main/java/turing/turing/domain/homework/HomeworkService.java index aec2d9a..39404ec 100644 --- a/src/main/java/turing/turing/domain/homework/HomeworkService.java +++ b/src/main/java/turing/turing/domain/homework/HomeworkService.java @@ -1,61 +1,18 @@ package turing.turing.domain.homework; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import turing.turing.domain.homework.converter.HomeworkConverter; import turing.turing.domain.homework.dto.CreateHomeworkRequest; import turing.turing.domain.homework.dto.DetailedHomeworkDto; -import turing.turing.domain.notebook.Notebook; -import turing.turing.domain.notebook.NotebookRepository; -import turing.turing.global.exception.RestApiException; -import turing.turing.global.exception.errorCode.CommonErrorCode; +import turing.turing.domain.homework.dto.UpdateHomeworkRequest; -@Service -@RequiredArgsConstructor -@Transactional -public class HomeworkService { +public interface HomeworkService { - private final HomeworkRepository homeworkRepository; - private final NotebookRepository notebookRepository; + DetailedHomeworkDto getHomework(Long homeworkId); - @Transactional(readOnly = true) - public DetailedHomeworkDto getHomework(Long homeworkId) { - Homework homework = homeworkRepository.findById(homeworkId) - .orElseThrow(() -> new RestApiException(CommonErrorCode.NOT_FOUND)); + Long createHomework(CreateHomeworkRequest request); - return HomeworkConverter.toDetailedDto(homework); - } + Long updateHomework(UpdateHomeworkRequest request); - public Long createHomework(CreateHomeworkRequest request) { - Notebook notebook = notebookRepository.findById(request.getNotebookId()) - .orElseThrow(() -> new RestApiException(CommonErrorCode.NOT_FOUND)); + Long updateDone(Long homeworkId); - Homework homework = homeworkRepository.save(HomeworkConverter.toEntity(request, notebook)); - - return homework.getId(); - } - - public Long updateHomework(DetailedHomeworkDto request) { - Homework homework = homeworkRepository.findById(request.getHomeworkId()) - .orElseThrow(() -> new RestApiException(CommonErrorCode.NOT_FOUND)); - - return homework.update(request); - } - - public Long updateDone(Long homeworkId) { - Homework homework = homeworkRepository.findById(homeworkId) - .orElseThrow(() -> new RestApiException(CommonErrorCode.NOT_FOUND)); - - Boolean nowDone = homework.getIsDone(); - - return homework.updateDone(!nowDone); - } - - public void deleteHomework(Long homeworkId) { - Homework homework = homeworkRepository.findById(homeworkId) - .orElseThrow(() -> new RestApiException(CommonErrorCode.NOT_FOUND)); - - homeworkRepository.delete(homework); - } + void deleteHomework(Long homeworkId); } diff --git a/src/main/java/turing/turing/domain/homework/HomeworkServiceImpl.java b/src/main/java/turing/turing/domain/homework/HomeworkServiceImpl.java new file mode 100644 index 0000000..cc64d7d --- /dev/null +++ b/src/main/java/turing/turing/domain/homework/HomeworkServiceImpl.java @@ -0,0 +1,65 @@ +package turing.turing.domain.homework; + +import static turing.turing.global.exception.errorCode.HomeworkErrorCode.*; +import static turing.turing.global.exception.errorCode.NotebookErrorCode.*; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import turing.turing.domain.homework.converter.HomeworkConverter; +import turing.turing.domain.homework.dto.CreateHomeworkRequest; +import turing.turing.domain.homework.dto.DetailedHomeworkDto; +import turing.turing.domain.homework.dto.UpdateHomeworkRequest; +import turing.turing.domain.notebook.Notebook; +import turing.turing.domain.notebook.NotebookRepository; +import turing.turing.global.exception.RestApiException; + +@Service +@RequiredArgsConstructor +@Transactional +public class HomeworkServiceImpl implements HomeworkService { + + private final HomeworkRepository homeworkRepository; + private final NotebookRepository notebookRepository; + + @Transactional(readOnly = true) + public DetailedHomeworkDto getHomework(Long homeworkId) { + Homework homework = findById(homeworkId); + + return HomeworkConverter.toDetailedDto(homework); + } + + public Long createHomework(CreateHomeworkRequest request) { + Notebook notebook = notebookRepository.findById(request.getNotebookId()) + .orElseThrow(() -> new RestApiException(NOTEBOOK_NOT_FOUND)); + + Homework homework = homeworkRepository.save(HomeworkConverter.toEntity(request, notebook)); + + return homework.getId(); + } + + public Long updateHomework(UpdateHomeworkRequest request) { + Homework homework = findById(request.getHomeworkId()); + + return homework.update(request); + } + + public Long updateDone(Long homeworkId) { + Homework homework = findById(homeworkId); + + Boolean nowDone = homework.getIsDone(); + + return homework.updateDone(!nowDone); + } + + public void deleteHomework(Long homeworkId) { + Homework homework = findById(homeworkId); + + homeworkRepository.delete(homework); + } + + private Homework findById(Long homeworkId) { + return homeworkRepository.findById(homeworkId) + .orElseThrow(() -> new RestApiException(HOMEWORK_NOT_FOUND)); + } +} diff --git a/src/main/java/turing/turing/domain/homework/converter/HomeworkConverter.java b/src/main/java/turing/turing/domain/homework/converter/HomeworkConverter.java index 438573e..22fe4b6 100644 --- a/src/main/java/turing/turing/domain/homework/converter/HomeworkConverter.java +++ b/src/main/java/turing/turing/domain/homework/converter/HomeworkConverter.java @@ -18,7 +18,6 @@ public static Homework toEntity (CreateHomeworkRequest request, Notebook noteboo .content(request.getContent()) .memo(request.getMemo()) .notebook(notebook) - .isDone(false) .build(); } diff --git a/src/main/java/turing/turing/domain/homework/dto/CreateHomeworkRequest.java b/src/main/java/turing/turing/domain/homework/dto/CreateHomeworkRequest.java index 7c17eaa..106edb8 100644 --- a/src/main/java/turing/turing/domain/homework/dto/CreateHomeworkRequest.java +++ b/src/main/java/turing/turing/domain/homework/dto/CreateHomeworkRequest.java @@ -1,23 +1,55 @@ package turing.turing.domain.homework.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; +import turing.turing.global.util.validation.range.ValidRange; @Getter +@NoArgsConstructor +@ValidRange(startField = "rangeStart", endField = "rangeEnd", type = Integer.class, message = "끝범위는 시작범위와 같거나 커야 합니다.") public class CreateHomeworkRequest { + @NotBlank(message = "카테고리는 필수입니다.") private String category; + @NotBlank(message = "제목은 필수입니다.") private String title; + @NotBlank(message = "범위타입은 필수입니다.") private String rangeType; + @NotNull(message = "시작범위는 필수입니다.") + @Positive(message = "시작범위는 양수여야 합니다.") private Integer rangeStart; + @NotNull(message = "끝범위는 필수입니다.") + @Positive(message = "끝범위는 양수여야 합니다.") private Integer rangeEnd; + @NotBlank(message = "내용은 필수입니다.") private String content; private String memo; + @NotNull(message = "알림장Id는 필수입니다.") + @Positive(message = "알림장Id는 양수여야 합니다.") private Long notebookId; + + @Builder + private CreateHomeworkRequest(String category, String title, String rangeType, + Integer rangeStart, + Integer rangeEnd, String content, String memo, Long notebookId) { + this.category = category; + this.title = title; + this.rangeType = rangeType; + this.rangeStart = rangeStart; + this.rangeEnd = rangeEnd; + this.content = content; + this.memo = memo; + this.notebookId = notebookId; + } } diff --git a/src/main/java/turing/turing/domain/homework/dto/UpdateHomeworkRequest.java b/src/main/java/turing/turing/domain/homework/dto/UpdateHomeworkRequest.java new file mode 100644 index 0000000..8f1f812 --- /dev/null +++ b/src/main/java/turing/turing/domain/homework/dto/UpdateHomeworkRequest.java @@ -0,0 +1,54 @@ +package turing.turing.domain.homework.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import turing.turing.global.util.validation.range.ValidRange; + +@Getter +@NoArgsConstructor +@ValidRange(startField = "rangeStart", endField = "rangeEnd", type = Integer.class, message = "끝범위는 시작범위와 같거나 커야 합니다.") +public class UpdateHomeworkRequest { + + @NotNull(message = "숙제Id는 필수입니다.") + @Positive(message = "숙제Id는 양수여야 합니다.") + private Long homeworkId; + + @NotBlank(message = "카테고리는 필수입니다.") + private String category; + + @NotBlank(message = "제목은 필수입니다.") + private String title; + + @NotBlank(message = "범위타입은 필수입니다.") + private String rangeType; + + @NotNull(message = "시작범위는 필수입니다.") + @Positive(message = "시작범위는 양수여야 합니다.") + private Integer rangeStart; + + @NotNull(message = "끝범위는 필수입니다.") + @Positive(message = "끝범위는 양수여야 합니다.") + private Integer rangeEnd; + + @NotBlank(message = "내용은 필수입니다.") + private String content; + + private String memo; + + @Builder + private UpdateHomeworkRequest(Long homeworkId, String category, String title, String rangeType, + Integer rangeStart, Integer rangeEnd, String content, String memo) { + this.homeworkId = homeworkId; + this.category = category; + this.title = title; + this.rangeType = rangeType; + this.rangeStart = rangeStart; + this.rangeEnd = rangeEnd; + this.content = content; + this.memo = memo; + } +} diff --git a/src/main/java/turing/turing/global/exception/errorCode/HomeworkErrorCode.java b/src/main/java/turing/turing/global/exception/errorCode/HomeworkErrorCode.java new file mode 100644 index 0000000..e0424d0 --- /dev/null +++ b/src/main/java/turing/turing/global/exception/errorCode/HomeworkErrorCode.java @@ -0,0 +1,15 @@ +package turing.turing.global.exception.errorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum HomeworkErrorCode implements ErrorCode { + + HOMEWORK_NOT_FOUND(HttpStatus.NOT_FOUND, "Homework Not Found"); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/turing/turing/global/exception/errorCode/NotebookErrorCode.java b/src/main/java/turing/turing/global/exception/errorCode/NotebookErrorCode.java new file mode 100644 index 0000000..25cf3ac --- /dev/null +++ b/src/main/java/turing/turing/global/exception/errorCode/NotebookErrorCode.java @@ -0,0 +1,15 @@ +package turing.turing.global.exception.errorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum NotebookErrorCode implements ErrorCode { + + NOTEBOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "Notebook Not Found"); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/turing/turing/global/util/validation/dateRange/DateRangeValidator.java b/src/main/java/turing/turing/global/util/validation/dateRange/DateRangeValidator.java deleted file mode 100644 index 2385c3a..0000000 --- a/src/main/java/turing/turing/global/util/validation/dateRange/DateRangeValidator.java +++ /dev/null @@ -1,46 +0,0 @@ -package turing.turing.global.util.validation.dateRange; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import java.lang.reflect.Field; -import java.time.LocalDate; -import turing.turing.global.exception.RestApiException; -import turing.turing.global.exception.errorCode.CommonErrorCode; - -public class DateRangeValidator implements ConstraintValidator { - - private String startDateField; - private String endDateField; - - @Override - public void initialize(ValidDateRange constraintAnnotation) { - this.startDateField = constraintAnnotation.startDateField(); - this.endDateField = constraintAnnotation.endDateField(); - } - - @Override - public boolean isValid(Object value, ConstraintValidatorContext context) { - try { - Field startField = value.getClass().getDeclaredField(startDateField); - Field endField = value.getClass().getDeclaredField(endDateField); - - startField.setAccessible(true); - endField.setAccessible(true); - - LocalDate startDate = (LocalDate) startField.get(value); - LocalDate endDate = (LocalDate) endField.get(value); - - if (startDate == null || endDate == null) { - return true; - } - - if (endDate.isBefore(startDate)) { - throw new RestApiException(CommonErrorCode.END_DATE_BEFORE_START_DATE); - } - - return true; - } catch (Exception e) { - return false; - } - } -} diff --git a/src/main/java/turing/turing/global/util/validation/range/DynamicRangeValidator.java b/src/main/java/turing/turing/global/util/validation/range/DynamicRangeValidator.java new file mode 100644 index 0000000..d3e245d --- /dev/null +++ b/src/main/java/turing/turing/global/util/validation/range/DynamicRangeValidator.java @@ -0,0 +1,44 @@ +package turing.turing.global.util.validation.range; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.lang.reflect.Field; + +public class DynamicRangeValidator implements ConstraintValidator { + + private String startField; + private String endField; + private Class type; + + @Override + public void initialize(ValidRange constraintAnnotation) { + this.startField = constraintAnnotation.startField(); + this.endField = constraintAnnotation.endField(); + this.type = constraintAnnotation.type(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + try { + Field startField = value.getClass().getDeclaredField(this.startField); + Field endField = value.getClass().getDeclaredField(this.endField); + + startField.setAccessible(true); + endField.setAccessible(true); + + Object startValue = startField.get(value); + Object endValue = endField.get(value); + + if (startValue == null || endValue == null) { + return true; + } + + RangeValidator validator = RangeValidatorFactory.getValidator(type); + + return validator.isValid(startValue, endValue); + + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/turing/turing/global/util/validation/range/RangeValidator.java b/src/main/java/turing/turing/global/util/validation/range/RangeValidator.java new file mode 100644 index 0000000..e30d666 --- /dev/null +++ b/src/main/java/turing/turing/global/util/validation/range/RangeValidator.java @@ -0,0 +1,6 @@ +package turing.turing.global.util.validation.range; + +public interface RangeValidator { + + boolean isValid(T start, T end); +} diff --git a/src/main/java/turing/turing/global/util/validation/range/RangeValidatorFactory.java b/src/main/java/turing/turing/global/util/validation/range/RangeValidatorFactory.java new file mode 100644 index 0000000..ed15b39 --- /dev/null +++ b/src/main/java/turing/turing/global/util/validation/range/RangeValidatorFactory.java @@ -0,0 +1,26 @@ +package turing.turing.global.util.validation.range; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import turing.turing.global.util.validation.range.date.DateRangeValidator; +import turing.turing.global.util.validation.range.number.NumberRangeValidator; + +public class RangeValidatorFactory { + + private static final Map, RangeValidator> validators = new HashMap<>(); + + static { + validators.put(LocalDate.class, new DateRangeValidator()); + validators.put(Integer.class, new NumberRangeValidator()); + } + + @SuppressWarnings("unchecked") + public static RangeValidator getValidator(Class type) { + RangeValidator validator = validators.get(type); + if (validator == null) { + throw new IllegalArgumentException("No validator found for type: " + type.getName()); + } + return (RangeValidator) validator; + } +} diff --git a/src/main/java/turing/turing/global/util/validation/dateRange/ValidDateRange.java b/src/main/java/turing/turing/global/util/validation/range/ValidRange.java similarity index 57% rename from src/main/java/turing/turing/global/util/validation/dateRange/ValidDateRange.java rename to src/main/java/turing/turing/global/util/validation/range/ValidRange.java index 452fbf9..5d8be3b 100644 --- a/src/main/java/turing/turing/global/util/validation/dateRange/ValidDateRange.java +++ b/src/main/java/turing/turing/global/util/validation/range/ValidRange.java @@ -1,4 +1,4 @@ -package turing.turing.global.util.validation.dateRange; +package turing.turing.global.util.validation.range; import jakarta.validation.Constraint; import jakarta.validation.Payload; @@ -9,14 +9,15 @@ import java.lang.annotation.Target; @Documented -@Constraint(validatedBy = DateRangeValidator.class) +@Constraint(validatedBy = DynamicRangeValidator.class) @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -public @interface ValidDateRange { - String message() default "종료일은 시작일과 같거나 미래여야 합니다."; +public @interface ValidRange { + String message() default ""; Class[] groups() default {}; Class[] payload() default {}; - String startDateField() default "startDate"; - String endDateField() default "endDate"; + String startField() default "start"; + String endField() default "end"; + Class type(); } diff --git a/src/main/java/turing/turing/global/util/validation/range/date/DateRangeValidator.java b/src/main/java/turing/turing/global/util/validation/range/date/DateRangeValidator.java new file mode 100644 index 0000000..fcff7bf --- /dev/null +++ b/src/main/java/turing/turing/global/util/validation/range/date/DateRangeValidator.java @@ -0,0 +1,12 @@ +package turing.turing.global.util.validation.range.date; + +import java.time.LocalDate; +import turing.turing.global.util.validation.range.RangeValidator; + +public class DateRangeValidator implements RangeValidator { + + @Override + public boolean isValid(LocalDate start, LocalDate end) { + return !end.isBefore(start); + } +} diff --git a/src/main/java/turing/turing/global/util/validation/range/number/NumberRangeValidator.java b/src/main/java/turing/turing/global/util/validation/range/number/NumberRangeValidator.java new file mode 100644 index 0000000..5d09e0b --- /dev/null +++ b/src/main/java/turing/turing/global/util/validation/range/number/NumberRangeValidator.java @@ -0,0 +1,11 @@ +package turing.turing.global.util.validation.range.number; + +import turing.turing.global.util.validation.range.RangeValidator; + +public class NumberRangeValidator implements RangeValidator { + + @Override + public boolean isValid(Integer start, Integer end) { + return !(end < start); + } +} diff --git a/src/test/java/turing/turing/domain/ControllerTestSupport.java b/src/test/java/turing/turing/domain/ControllerTestSupport.java index 672a378..7b2c61f 100644 --- a/src/test/java/turing/turing/domain/ControllerTestSupport.java +++ b/src/test/java/turing/turing/domain/ControllerTestSupport.java @@ -9,10 +9,13 @@ import turing.turing.domain.auth.CustomUserDetails; import turing.turing.domain.exam.ExamController; import turing.turing.domain.exam.ExamService; +import turing.turing.domain.homework.HomeworkController; +import turing.turing.domain.homework.HomeworkService; import turing.turing.global.security.JwtAuthenticationFilter; @WebMvcTest(controllers = { - ExamController.class + ExamController.class, + HomeworkController.class }) @AutoConfigureMockMvc(addFilters = false) public abstract class ControllerTestSupport { @@ -23,12 +26,15 @@ public abstract class ControllerTestSupport { @Autowired protected ObjectMapper objectMapper; - @MockBean - protected ExamService examService; - @MockBean protected JwtAuthenticationFilter jwtAuthenticationFilter; @MockBean protected CustomUserDetails customUserDetails; + + @MockBean + protected ExamService examService; + + @MockBean + protected HomeworkService homeworkService; } diff --git a/src/test/java/turing/turing/domain/exam/ExamControllerTest.java b/src/test/java/turing/turing/domain/exam/ExamControllerTest.java index b80979e..6b577b3 100644 --- a/src/test/java/turing/turing/domain/exam/ExamControllerTest.java +++ b/src/test/java/turing/turing/domain/exam/ExamControllerTest.java @@ -38,12 +38,12 @@ void createExamSchedule() throws Exception { // when // then mockMvc.perform( - post("/exam") + post("/api/exam") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ).andDo(print()) .andExpect(status().isCreated()) - .andExpect(header().string("Location", "http://localhost/exam/1")); + .andExpect(header().string("Location", "http://localhost/api/exam/1")); } @DisplayName("시험을 등록할 때 시험명은 필수값이다.") @@ -66,7 +66,7 @@ void createExamScheduleWithoutExamName() throws Exception { // when // then mockMvc.perform( - post("/exam") + post("/api/exam") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ).andDo(print()) @@ -94,7 +94,7 @@ void createExamScheduleWithEndDateBeforeStartDate() throws Exception { // when // then mockMvc.perform( - post("/exam") + post("/api/exam") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ).andDo(print()) @@ -121,7 +121,7 @@ void test() throws Exception { // when // then mockMvc.perform( - post("/exam") + post("/api/exam") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ).andDo(print()) @@ -145,7 +145,7 @@ void modifyExamSchedule() throws Exception { .thenReturn(request.getExamId()); // when then mockMvc.perform( - patch("/exam") + patch("/api/exam") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ).andDo(print()) @@ -168,7 +168,7 @@ void modifyExamScheduleWithoutExamId() throws Exception { .thenReturn(request.getExamId()); // when then mockMvc.perform( - patch("/exam") + patch("/api/exam") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ).andDo(print()) diff --git a/src/test/java/turing/turing/domain/homework/HomeworkControllerTest.java b/src/test/java/turing/turing/domain/homework/HomeworkControllerTest.java new file mode 100644 index 0000000..18b36f7 --- /dev/null +++ b/src/test/java/turing/turing/domain/homework/HomeworkControllerTest.java @@ -0,0 +1,214 @@ +package turing.turing.domain.homework; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import turing.turing.domain.ControllerTestSupport; +import turing.turing.domain.homework.dto.CreateHomeworkRequest; + +public class HomeworkControllerTest extends ControllerTestSupport { + + @DisplayName("숙제를 생성한다.") + @Test + void createHomework() throws Exception { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .notebookId(1L) + .build(); + + when(homeworkService.createHomework(any(CreateHomeworkRequest.class))) + .thenReturn(1L); + + // when // then + mockMvc.perform( + post("/api/homework") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", "http://localhost/api/homework/1")); + } + + @DisplayName("숙제를 생성할 때 카테고리는 필수입니다.") + @Test + void createHomeworkWithoutCategory() throws Exception { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .notebookId(1L) + .build(); + + when(homeworkService.createHomework(any(CreateHomeworkRequest.class))) + .thenReturn(1L); + + // when // then + mockMvc.perform( + post("/api/homework") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message").value("카테고리는 필수입니다.")); + } + + @DisplayName("숙제를 생성할 때 제목은 필수입니다.") + @Test + void createHomeworkWithoutTitle() throws Exception { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .category("카테고리") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .notebookId(1L) + .build(); + + when(homeworkService.createHomework(any(CreateHomeworkRequest.class))) + .thenReturn(1L); + + // when // then + mockMvc.perform( + post("/api/homework") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message").value("제목은 필수입니다.")); + } + + @DisplayName("숙제를 생성할 때 범위타입은 필수입니다.") + @Test + void createHomeworkWithoutRangeType() throws Exception { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .category("카테고리") + .title("제목") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .notebookId(1L) + .build(); + + when(homeworkService.createHomework(any(CreateHomeworkRequest.class))) + .thenReturn(1L); + + // when // then + mockMvc.perform( + post("/api/homework") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message").value("범위타입은 필수입니다.")); + } + + @DisplayName("숙제를 생성할 때 끝범위는 시작범위보다 크거나 같아야 합니다.") + @Test + void createHomeworkWithRangeStartBiggerThanEnd() throws Exception { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(10) + .rangeEnd(1) + .content("내용") + .notebookId(1L) + .build(); + + when(homeworkService.createHomework(any(CreateHomeworkRequest.class))) + .thenReturn(1L); + + // when // then + mockMvc.perform( + post("/api/homework") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message").value("끝범위는 시작범위와 같거나 커야 합니다.")); + } + + @DisplayName("숙제를 생성할 때 내용은 필수입니다.") + @Test + void createHomeworkWithoutContent() throws Exception { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .notebookId(1L) + .build(); + + when(homeworkService.createHomework(any(CreateHomeworkRequest.class))) + .thenReturn(1L); + + // when // then + mockMvc.perform( + post("/api/homework") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message").value("내용은 필수입니다.")); + } + + @DisplayName("숙제를 생성할 때 알림장Id는 필수입니다.") + @Test + void createHomeworkWithoutNotebookId() throws Exception { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .build(); + + when(homeworkService.createHomework(any(CreateHomeworkRequest.class))) + .thenReturn(1L); + + // when // then + mockMvc.perform( + post("/api/homework") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].message").value("알림장Id는 필수입니다.")); + } + + @DisplayName("숙제를 삭제한다.") + @Test + void deleteHomework() throws Exception { + // given // when // then + Long homeworkId = 1L; + mockMvc.perform( + delete("/api/homework/{id}", homeworkId) + ).andExpect(status().isNoContent()); + } + +} diff --git a/src/test/java/turing/turing/domain/homework/HomeworkServiceImplTest.java b/src/test/java/turing/turing/domain/homework/HomeworkServiceImplTest.java new file mode 100644 index 0000000..2e0c347 --- /dev/null +++ b/src/test/java/turing/turing/domain/homework/HomeworkServiceImplTest.java @@ -0,0 +1,264 @@ +package turing.turing.domain.homework; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static turing.turing.global.exception.errorCode.HomeworkErrorCode.HOMEWORK_NOT_FOUND; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import turing.turing.domain.IntegrationTestSupport; +import turing.turing.domain.homework.dto.CreateHomeworkRequest; +import turing.turing.domain.homework.dto.DetailedHomeworkDto; +import turing.turing.domain.homework.dto.UpdateHomeworkRequest; +import turing.turing.domain.member.Provider; +import turing.turing.domain.member.Role; +import turing.turing.domain.notebook.Notebook; +import turing.turing.domain.notebook.NotebookRepository; +import turing.turing.domain.schedule.Schedule; +import turing.turing.domain.schedule.ScheduleRepository; +import turing.turing.domain.student.Student; +import turing.turing.domain.student.StudentRepository; +import turing.turing.domain.studyRoom.StudyRoom; +import turing.turing.domain.studyRoom.StudyRoomRepository; +import turing.turing.domain.teacher.Teacher; +import turing.turing.domain.teacher.TeacherRepository; +import turing.turing.global.exception.RestApiException; + +class HomeworkServiceImplTest extends IntegrationTestSupport { + + @Autowired + private TeacherRepository teacherRepository; + @Autowired + private StudentRepository studentRepository; + @Autowired + private StudyRoomRepository studyRoomRepository; + @Autowired + private ScheduleRepository scheduleRepository; + @Autowired + private NotebookRepository notebookRepository; + + @Autowired + private HomeworkService homeworkService; + @Autowired + private HomeworkRepository homeworkRepository; + private Notebook notebook; + + @BeforeEach + void setUp() { + Teacher teacher = new Teacher("teacher@naver.com", Role.TEACHER, Provider.KAKAO, "범준", "김"); + Student student = new Student("student@naver.com", Role.STUDENT, Provider.KAKAO, "학생이름", "학생성"); + teacherRepository.save(teacher); + studentRepository.save(student); + StudyRoom studyRoom = new StudyRoom("과목", 8, 20000, teacher, student); + studyRoomRepository.save(studyRoom); + Schedule schedule = new Schedule(LocalDate.now(), LocalTime.now(), LocalTime.now(), "학생이름", "과목", 8, studyRoom); + scheduleRepository.save(schedule); + notebook = new Notebook(schedule, Timestamp.valueOf("2025-12-25 18:00:00")); + notebookRepository.save(notebook); + } + + @DisplayName("숙제Id를 통해 숙제 정보를 조회할 수 있다.") + @Test + void getHomework() { + // given + Homework homework = Homework.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .memo("메모") + .notebook(notebook) + .build(); + + Long savedId = homeworkRepository.save(homework).getId(); + + // when + DetailedHomeworkDto response = homeworkService.getHomework(savedId); + // then + assertThat(response) + .extracting("homeworkId", "category", "title", "rangeType", "rangeStart", + "rangeEnd", "content", "memo") + .contains(savedId, "카테고리", "제목", "범위타입", 1, 10, "내용", "메모"); + } + + @DisplayName("숙제를 생성할 수 있다.") + @Test + void createHomework() { + // given + CreateHomeworkRequest request = CreateHomeworkRequest.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .notebookId(1L) + .build(); + // when + Long savedId = homeworkService.createHomework(request); + + // then + assertThat(homeworkRepository.findById(savedId).get()) + .extracting("id", "category", "title", "rangeType", "rangeStart", "rangeEnd", + "content", "memo", "isDone") + .contains(1L, "카테고리", "제목", + "범위타입", 1, 10, "내용", "메모", false); + } + + @DisplayName("요청 정보로 숙제를 수정할 수 있다.") + @Test + void updateHomework() { + // given + Homework homework = Homework.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .memo("메모") + .notebook(notebook) + .build(); + Long savedId = homeworkRepository.save(homework).getId(); + + UpdateHomeworkRequest request = UpdateHomeworkRequest.builder() + .homeworkId(savedId) + .category("새로운 카테고리") + .title("새로운 제목") + .rangeType("새로운 범위타입") + .rangeStart(11) + .rangeEnd(20) + .content("새로운 내용") + .memo("새로운 메모") + .build(); + // when + homeworkService.updateHomework(request); + + // then + assertThat(homeworkRepository.findById(savedId).get()) + .extracting("id", "category", "title", "rangeType", "rangeStart", "rangeEnd", + "content", "memo", "isDone") + .contains(1L, "새로운 카테고리", "새로운 제목", + "새로운 범위타입", 11, 20, "새로운 내용", "새로운 메모", false); + } + + @DisplayName("숙제 완료여부를 완료로 수정할 수 있다.") + @Test + void updateDone() { + // given + Homework homework = Homework.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .memo("메모") + .notebook(notebook) + .build(); + + Long savedId = homeworkRepository.save(homework).getId(); + // when + homeworkService.updateDone(savedId); + + // then + assertThat(homeworkRepository.findById(savedId).get().getIsDone()).isTrue(); + } + + @DisplayName("숙제 완료여부를 미완료로 수정할 수 있다.") + @Test + void updateNotDone() { + // given + Homework homework = Homework.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .memo("메모") + .notebook(notebook) + .build(); + + Long savedId = homeworkRepository.save(homework).getId(); + // when + homeworkService.updateDone(savedId); + homeworkService.updateDone(savedId); + // then + assertThat(homeworkRepository.findById(savedId).get().getIsDone()).isFalse(); + } + + @DisplayName("숙제를 삭제할 수 있다.") + @Test + void deleteHomework() { + // given + Homework homework = Homework.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .memo("메모") + .notebook(notebook) + .build(); + Long savedId = homeworkRepository.save(homework).getId(); + + // when + homeworkService.deleteHomework(savedId); + + // then + Optional deletedHomework = homeworkRepository.findById(savedId); + assertThat(deletedHomework).isEmpty(); + } + + @DisplayName("숙제Id를 통해 숙제 엔티티를 조회할 수 있다.") + @Test + void findById() { + // given + Homework homework = Homework.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .memo("메모") + .notebook(notebook) + .build(); + Long savedId = homeworkRepository.save(homework).getId(); + + // when // then + assertThat(homework).isEqualTo(homeworkRepository.findById(savedId).get()); + } + + @DisplayName("존재하지 않는 Id로 조회할 경우 에러가 발생한다.") + @Test + void findByIdWithNotExistId() { + // given + Homework homework = Homework.builder() + .category("카테고리") + .title("제목") + .rangeType("범위타입") + .rangeStart(1) + .rangeEnd(10) + .content("내용") + .memo("메모") + .notebook(notebook) + .build(); + Long savedId = homeworkRepository.save(homework).getId(); + + // when // then + assertThatThrownBy(() -> homeworkService.getHomework(savedId + 1)) + .isInstanceOf(RestApiException.class) + .extracting("ErrorCode").isEqualTo(HOMEWORK_NOT_FOUND); + } +} \ No newline at end of file