diff --git a/.gitignore b/.gitignore index 767de451..067c7557 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ build/ /src/main/resources/application-prod.yml /src/main/resources/application-dev.yml /src/main/resources/application-local.yml -/src/test/resources/*.yml *.json *.DS_Store diff --git a/src/main/java/usw/suwiki/domain/evaluatepost/service/EvaluatePostService.java b/src/main/java/usw/suwiki/domain/evaluatepost/service/EvaluatePostService.java index 63140644..0e6b6d1b 100644 --- a/src/main/java/usw/suwiki/domain/evaluatepost/service/EvaluatePostService.java +++ b/src/main/java/usw/suwiki/domain/evaluatepost/service/EvaluatePostService.java @@ -1,5 +1,9 @@ package usw.suwiki.domain.evaluatepost.service; +import static usw.suwiki.global.exception.ExceptionType.POSTS_WRITE_OVERLAP; + +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,28 +16,25 @@ import usw.suwiki.domain.evaluatepost.service.dto.FindByLectureToJson; import usw.suwiki.domain.lecture.domain.Lecture; import usw.suwiki.domain.lecture.service.LectureCRUDService; +import usw.suwiki.domain.lecture.service.LectureService; import usw.suwiki.domain.user.user.User; import usw.suwiki.domain.user.user.service.UserCRUDService; import usw.suwiki.global.PageOption; import usw.suwiki.global.exception.errortype.EvaluatePostException; -import java.util.ArrayList; -import java.util.List; - -import static usw.suwiki.global.exception.ExceptionType.POSTS_WRITE_OVERLAP; - @Service @RequiredArgsConstructor public class EvaluatePostService { private final EvaluatePostCRUDService evaluatePostCRUDService; private final LectureCRUDService lectureCRUDService; + private final LectureService lectureService; private final UserCRUDService userCRUDService; @Transactional public void write(EvaluatePostSaveDto evaluatePostData, Long userIdx, Long lectureId) { checkAlreadyWrite(userIdx, lectureId); - Lecture lecture = lectureCRUDService.loadLectureFromId(lectureId); + Lecture lecture = lectureService.findLectureById(lectureId); User user = userCRUDService.loadUserFromUserIdx(userIdx); EvaluatePost evaluatePost = createEvaluatePost(evaluatePostData, user, lecture); @@ -104,7 +105,7 @@ public List readEvaluatePostsByUserId( } public boolean verifyIsUserCanWriteEvaluatePost(Long userIdx, Long lectureId) { - Lecture lecture = lectureCRUDService.loadLectureFromId(lectureId); + Lecture lecture = lectureService.findLectureById(lectureId);; User user = userCRUDService.loadUserFromUserIdx(userIdx); return evaluatePostCRUDService.isAlreadyWritten(user, lecture); } diff --git a/src/main/java/usw/suwiki/domain/exampost/service/ExamPostService.java b/src/main/java/usw/suwiki/domain/exampost/service/ExamPostService.java index 654c249f..7279b5a3 100644 --- a/src/main/java/usw/suwiki/domain/exampost/service/ExamPostService.java +++ b/src/main/java/usw/suwiki/domain/exampost/service/ExamPostService.java @@ -1,35 +1,37 @@ package usw.suwiki.domain.exampost.service; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -import usw.suwiki.domain.exampost.controller.dto.*; +import usw.suwiki.domain.exampost.controller.dto.ExamPostUpdateDto; +import usw.suwiki.domain.exampost.controller.dto.ExamPostsSaveDto; +import usw.suwiki.domain.exampost.controller.dto.ExamResponseByLectureIdDto; +import usw.suwiki.domain.exampost.controller.dto.ExamResponseByUserIdxDto; +import usw.suwiki.domain.exampost.controller.dto.ReadExamPostResponse; import usw.suwiki.domain.exampost.controller.dto.viewexam.PurchaseHistoryDto; import usw.suwiki.domain.exampost.domain.ExamPost; -import usw.suwiki.domain.userlecture.viewexam.ViewExam; import usw.suwiki.domain.lecture.domain.Lecture; -import usw.suwiki.domain.lecture.service.LectureCRUDService; +import usw.suwiki.domain.lecture.service.LectureService; import usw.suwiki.domain.user.user.User; import usw.suwiki.domain.user.user.service.UserCRUDService; +import usw.suwiki.domain.userlecture.viewexam.ViewExam; import usw.suwiki.domain.userlecture.viewexam.service.ViewExamCRUDService; import usw.suwiki.global.PageOption; -import java.util.ArrayList; -import java.util.List; - @Service @RequiredArgsConstructor public class ExamPostService { private final ExamPostCRUDService examPostCRUDService; private final ViewExamCRUDService viewExamCRUDService; - private final LectureCRUDService lectureCRUDService; + private final LectureService lectureService; private final UserCRUDService userCRUDService; @Transactional public void write(ExamPostsSaveDto examData, Long userIdx, Long lectureId) { - Lecture lecture = lectureCRUDService.loadLectureFromId(lectureId); + Lecture lecture = lectureService.findLectureById(lectureId); User user = userCRUDService.loadUserFromUserIdx(userIdx); ExamPost examPost = createExamPost(examData, user, lecture); @@ -41,7 +43,7 @@ public void write(ExamPostsSaveDto examData, Long userIdx, Long lectureId) { @Transactional public void purchase(Long lectureIdx, Long userIdx) { User user = userCRUDService.loadUserFromUserIdx(userIdx); - Lecture lecture = lectureCRUDService.loadLectureFromId(lectureIdx); + Lecture lecture =lectureService.findLectureById(lectureIdx); user.purchaseExamPost(); ViewExam viewExam = ViewExam.builder() @@ -109,7 +111,7 @@ public List readExamPostByUserIdAndOption(PageOption o @Transactional(readOnly = true) public boolean isWrite(Long userIdx, Long lectureIdx) { User user = userCRUDService.loadUserFromUserIdx(userIdx); - Lecture lecture = lectureCRUDService.loadLectureFromId(lectureIdx); + Lecture lecture = lectureService.findLectureById(lectureIdx); return examPostCRUDService.isWrite(user, lecture); } diff --git a/src/main/java/usw/suwiki/domain/lecture/controller/LectureController.java b/src/main/java/usw/suwiki/domain/lecture/controller/LectureController.java index 1dd7be6d..3029663d 100644 --- a/src/main/java/usw/suwiki/domain/lecture/controller/LectureController.java +++ b/src/main/java/usw/suwiki/domain/lecture/controller/LectureController.java @@ -1,22 +1,30 @@ package usw.suwiki.domain.lecture.controller; +import static usw.suwiki.global.exception.ExceptionType.USER_RESTRICTED; + import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import usw.suwiki.domain.lecture.controller.dto.LectureAndCountResponseForm; import usw.suwiki.domain.lecture.controller.dto.LectureDetailResponseDto; import usw.suwiki.domain.lecture.controller.dto.LectureFindOption; +import usw.suwiki.domain.lecture.controller.dto.LectureWithScheduleResponse; import usw.suwiki.domain.lecture.service.LectureService; import usw.suwiki.global.ResponseForm; import usw.suwiki.global.annotation.ApiLogger; import usw.suwiki.global.annotation.CacheStatics; +import usw.suwiki.global.dto.ApiResponse; +import usw.suwiki.global.dto.NoOffsetPaginationResponse; import usw.suwiki.global.exception.errortype.AccountException; import usw.suwiki.global.jwt.JwtAgent; import usw.suwiki.global.util.CacheStaticsLogger; -import static usw.suwiki.global.exception.ExceptionType.USER_RESTRICTED; - @RestController @RequiredArgsConstructor @@ -43,6 +51,20 @@ public ResponseEntity searchLectureApi( return ResponseEntity.ok(response); } + @GetMapping("/current/cells/search") + public ResponseEntity>> searchLectureCells( + @RequestParam(required = false) Long cursorId, + @RequestParam(required = false, defaultValue = "20") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String major, + @RequestParam(required = false) Integer grade + ) { + NoOffsetPaginationResponse response = + lectureService.findPagedLecturesWithSchedule(cursorId, size, keyword, major, grade); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + @Cacheable(cacheNames = "lecture") @ApiLogger(option = "lecture") @CacheStatics diff --git a/src/main/java/usw/suwiki/domain/lecture/controller/dto/LectureWithScheduleResponse.java b/src/main/java/usw/suwiki/domain/lecture/controller/dto/LectureWithScheduleResponse.java new file mode 100644 index 00000000..0fb4ac1f --- /dev/null +++ b/src/main/java/usw/suwiki/domain/lecture/controller/dto/LectureWithScheduleResponse.java @@ -0,0 +1,42 @@ +package usw.suwiki.domain.lecture.controller.dto; + +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import usw.suwiki.domain.lecture.domain.Lecture; + +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LectureWithScheduleResponse { + private long id; + private String name; + private String type; + private String major; + private int grade; + private String professorName; + + private final List originalCellList = new ArrayList<>(); + + public static LectureWithScheduleResponse of( + Lecture lecture + ) { + return LectureWithScheduleResponse.builder() + .id(lecture.getId()) + .name(lecture.getName()) + .professorName(lecture.getProfessor()) + .type(lecture.getType()) + .major(lecture.getMajorType()) + .grade(lecture.getLectureDetail().getGrade()) + .build(); + } + + public void addOriginalCellResponse(OriginalLectureCellResponse cellResponse) { + this.originalCellList.add(cellResponse); + } +} diff --git a/src/main/java/usw/suwiki/domain/lecture/controller/dto/OriginalLectureCellResponse.java b/src/main/java/usw/suwiki/domain/lecture/controller/dto/OriginalLectureCellResponse.java new file mode 100644 index 00000000..979398f2 --- /dev/null +++ b/src/main/java/usw/suwiki/domain/lecture/controller/dto/OriginalLectureCellResponse.java @@ -0,0 +1,28 @@ +package usw.suwiki.domain.lecture.controller.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import usw.suwiki.domain.timetable.entity.TimetableCellSchedule; + +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class OriginalLectureCellResponse { + private String location; + private String day; + private Integer startPeriod; + private Integer endPeriod; + + public static OriginalLectureCellResponse of(TimetableCellSchedule schedule) { + return OriginalLectureCellResponse.builder() + .location(schedule.getLocation()) + .day(schedule.getDay().getValue()) + .startPeriod(schedule.getStartPeriod()) + .endPeriod(schedule.getEndPeriod()) + .build(); + } +} diff --git a/src/main/java/usw/suwiki/domain/lecture/domain/LectureDetail.java b/src/main/java/usw/suwiki/domain/lecture/domain/LectureDetail.java index 2bef40e8..afbd9362 100644 --- a/src/main/java/usw/suwiki/domain/lecture/domain/LectureDetail.java +++ b/src/main/java/usw/suwiki/domain/lecture/domain/LectureDetail.java @@ -2,7 +2,6 @@ import javax.persistence.Column; import javax.persistence.Embeddable; - import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,7 +9,7 @@ @Getter @Embeddable @NoArgsConstructor -public class LectureDetail { +public class LectureDetail { // TODO refactor: placeSchedule, grade, point 은 Lecture로 이동. 혹은 전체 이동. @Column(name = "place_schedule") private String placeSchedule; diff --git a/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureQueryRepository.java b/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureCustomRepository.java similarity index 71% rename from src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureQueryRepository.java rename to src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureCustomRepository.java index f9c84b94..3629937a 100644 --- a/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureQueryRepository.java +++ b/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureCustomRepository.java @@ -1,21 +1,30 @@ package usw.suwiki.domain.lecture.domain.repository; -import org.springframework.data.jpa.repository.Query; +import java.util.List; +import org.springframework.data.domain.Slice; import usw.suwiki.domain.lecture.controller.dto.LectureFindOption; import usw.suwiki.domain.lecture.domain.Lecture; import usw.suwiki.domain.lecture.domain.repository.dao.LecturesAndCountDao; -import java.util.List; +public interface LectureCustomRepository { + Slice findCurrentSemesterLectures( + final Long cursorId, + final int limit, + final String keyword, + final String majorType, + final Integer grade + ); -public interface LectureQueryRepository { - // Lecture verifyJsonLecture(String lectureName, String ProfessorName, String majorType); - // + List findAllMajorType(); - // + LecturesAndCountDao findLectureByFindOption(String searchValue, LectureFindOption lectureFindOption); + LecturesAndCountDao findLectureByMajorType(String searchValue, LectureFindOption lectureFindOption); + LecturesAndCountDao findAllLectureByFindOption(LectureFindOption lectureFindOption); + LecturesAndCountDao findAllLectureByMajorType(LectureFindOption lectureFindOption); } diff --git a/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureQueryRepositoryImpl.java b/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureCustomRepositoryImpl.java similarity index 78% rename from src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureQueryRepositoryImpl.java rename to src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureCustomRepositoryImpl.java index 901eec90..a69606a2 100644 --- a/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureQueryRepositoryImpl.java +++ b/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureCustomRepositoryImpl.java @@ -6,23 +6,52 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import usw.suwiki.domain.lecture.controller.dto.LectureFindOption; import usw.suwiki.domain.lecture.domain.Lecture; import usw.suwiki.domain.lecture.domain.repository.dao.LecturesAndCountDao; +import usw.suwiki.global.util.query.SlicePaginationUtils; @RequiredArgsConstructor -public class LectureQueryRepositoryImpl implements LectureQueryRepository { +public class LectureCustomRepositoryImpl implements LectureCustomRepository { // TODO style: Repository명 변경 private final JPAQueryFactory queryFactory; private final String DEFAULT_ORDER = "modifiedDate"; private final Integer DEFAULT_PAGE = 1; private final Integer DEFAULT_LIMIT = 10; + @Value("${business.current-semester}") + private String currentSemester; // TODO 고민: Lecture - currently_opened 혹은 last_opened_semester 컬럼 추가 -> 데이터 파싱 로직 및 WHERE절 변경해야 함. + + @Override + public Slice findCurrentSemesterLectures( + final Long cursorId, + final int limit, + final String keyword, + final String majorType, + final Integer grade + ) { + JPAQuery query = queryFactory.selectFrom(lecture) + .where(gtCursorId(cursorId)) + .where(containsKeyword(keyword)) + .where(eqMajorType(majorType)) + .where(eqGrade(grade)) + .where(lecture.semester.endsWith(currentSemester)) + .orderBy(lecture.id.asc()) + .limit(SlicePaginationUtils.increaseSliceLimit(limit)); + + return SlicePaginationUtils.buildSlice(query.fetch(), limit); + } + + @Override public Lecture verifyJsonLecture(String lectureName, String professorName, String majorType) { Lecture result = queryFactory @@ -38,12 +67,9 @@ public Lecture verifyJsonLecture(String lectureName, String professorName, Strin } - - /** - * if (!Arrays.asList(orderOptions).contains(orderOption)) { - * throw new AccountException(ExceptionType.INVALID_ORDER_OPTION); - * } + * if (!Arrays.asList(orderOptions).contains(orderOption)) { throw new + * AccountException(ExceptionType.INVALID_ORDER_OPTION); } */ @Override public LecturesAndCountDao findLectureByFindOption(String searchValue, LectureFindOption option) { @@ -54,7 +80,6 @@ public LecturesAndCountDao findLectureByFindOption(String searchValue, LectureFi .likeIgnoreCase("%" + searchValue + "%") .or(lecture.professor.likeIgnoreCase("%" + searchValue + "%")); - OrderSpecifier orderSpecifier = getOrderSpecifier(orderOption); Pageable pageable = PageRequest.of(page - 1, DEFAULT_LIMIT); @@ -178,6 +203,34 @@ public List findAllMajorType() { .fetch(); } + private BooleanExpression gtCursorId(Long cursorId) { + if (Objects.isNull(cursorId)) { + return null; + } + return lecture.id.gt(cursorId); + } + + private BooleanExpression containsKeyword(String keyword) { + if (Objects.isNull(keyword)) { + return null; + } + return lecture.name.contains(keyword); + } + + private BooleanExpression eqMajorType(String majorType) { + if (Objects.isNull(majorType)) { + return null; + } + return lecture.majorType.eq(majorType); + } + + private BooleanExpression eqGrade(Integer grade) { + if (Objects.isNull(grade)) { + return null; + } + return lecture.lectureDetail.grade.eq(grade); + } + private OrderSpecifier getOrderSpecifier(String orderOption) { switch (orderOption) { case "lectureEvaluationInfo.lectureSatisfactionAvg": diff --git a/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureRepository.java b/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureRepository.java index b905b2a1..d0797112 100644 --- a/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureRepository.java +++ b/src/main/java/usw/suwiki/domain/lecture/domain/repository/LectureRepository.java @@ -1,15 +1,14 @@ package usw.suwiki.domain.lecture.domain.repository; import java.util.Optional; +import javax.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.stereotype.Repository; import usw.suwiki.domain.lecture.domain.Lecture; -import javax.persistence.LockModeType; - @Repository -public interface LectureRepository extends JpaRepository, LectureQueryRepository { +public interface LectureRepository extends JpaRepository, LectureCustomRepository { // TODO: 낙관적 락은 어떨지 고민해보기 (김영한님의 추천은 READ COMMITTED + 낙관적 락) @Lock(value = LockModeType.PESSIMISTIC_WRITE) // TODO: timeout 필요성 동시성 테스트로 확인하기 diff --git a/src/main/java/usw/suwiki/domain/lecture/service/LectureCRUDService.java b/src/main/java/usw/suwiki/domain/lecture/service/LectureCRUDService.java index a07bb5aa..9446ae5b 100644 --- a/src/main/java/usw/suwiki/domain/lecture/service/LectureCRUDService.java +++ b/src/main/java/usw/suwiki/domain/lecture/service/LectureCRUDService.java @@ -1,17 +1,14 @@ package usw.suwiki.domain.lecture.service; -import static usw.suwiki.global.exception.ExceptionType.*; +import static usw.suwiki.global.exception.ExceptionType.LECTURE_NOT_FOUND; import java.util.List; -import java.util.Optional; - -import org.springframework.stereotype.Service; - import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; import usw.suwiki.domain.lecture.controller.dto.LectureFindOption; -import usw.suwiki.domain.lecture.domain.repository.dao.LecturesAndCountDao; import usw.suwiki.domain.lecture.domain.Lecture; import usw.suwiki.domain.lecture.domain.repository.LectureRepository; +import usw.suwiki.domain.lecture.domain.repository.dao.LecturesAndCountDao; import usw.suwiki.global.exception.errortype.LectureException; @Service @@ -20,11 +17,6 @@ public class LectureCRUDService { private final LectureRepository lectureRepository; - public Lecture loadLectureFromId(Long id) { - Optional lecture = lectureRepository.findById(id); - return validateOptional(lecture); - } - public List loadMajorTypes() { List majors = lectureRepository.findAllMajorType(); return majors; @@ -51,11 +43,4 @@ public LecturesAndCountDao loadLecturesByMajor(LectureFindOption option) { return lectureRepository.findAllLectureByMajorType(option); } - - public Lecture validateOptional(Optional lecture) { - if (lecture.isEmpty()) { - throw new LectureException(LECTURE_NOT_FOUND); - } - return lecture.get(); - } } diff --git a/src/main/java/usw/suwiki/domain/lecture/service/LectureService.java b/src/main/java/usw/suwiki/domain/lecture/service/LectureService.java index ab478e0b..82d501e8 100644 --- a/src/main/java/usw/suwiki/domain/lecture/service/LectureService.java +++ b/src/main/java/usw/suwiki/domain/lecture/service/LectureService.java @@ -1,26 +1,35 @@ package usw.suwiki.domain.lecture.service; +import static usw.suwiki.global.exception.ExceptionType.LECTURE_NOT_FOUND; + +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - +import usw.suwiki.domain.lecture.controller.dto.LectureAndCountResponseForm; import usw.suwiki.domain.lecture.controller.dto.LectureDetailResponseDto; -import usw.suwiki.domain.lecture.domain.repository.dao.LecturesAndCountDao; +import usw.suwiki.domain.lecture.controller.dto.LectureFindOption; import usw.suwiki.domain.lecture.controller.dto.LectureResponseDto; -import usw.suwiki.domain.lecture.controller.dto.LectureAndCountResponseForm; +import usw.suwiki.domain.lecture.controller.dto.LectureWithScheduleResponse; +import usw.suwiki.domain.lecture.controller.dto.OriginalLectureCellResponse; import usw.suwiki.domain.lecture.domain.Lecture; -import usw.suwiki.domain.lecture.controller.dto.LectureFindOption; - -import java.util.ArrayList; -import java.util.List; +import usw.suwiki.domain.lecture.domain.repository.LectureRepository; +import usw.suwiki.domain.lecture.domain.repository.dao.LecturesAndCountDao; +import usw.suwiki.domain.lecture.util.LectureStringConverter; +import usw.suwiki.domain.timetable.entity.TimetableCellSchedule; +import usw.suwiki.global.dto.NoOffsetPaginationResponse; +import usw.suwiki.global.exception.errortype.LectureException; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class LectureService { private final LectureCRUDService lectureCRUDService; + private final LectureRepository lectureRepository; - @Transactional(readOnly = true) public LectureAndCountResponseForm readLectureByKeyword(String keyword, LectureFindOption option) { if (option.passMajorFiltering()) { return readLectureByKeywordAndOption(keyword, option); @@ -28,7 +37,6 @@ public LectureAndCountResponseForm readLectureByKeyword(String keyword, LectureF return readLectureByKeywordAndMajor(keyword, option); } - @Transactional(readOnly = true) public LectureAndCountResponseForm readAllLecture(LectureFindOption option) { if (option.passMajorFiltering()) { return readAllLectureByOption(option); @@ -36,12 +44,48 @@ public LectureAndCountResponseForm readAllLecture(LectureFindOption option) { return readAllLectureByMajorType(option); } - @Transactional(readOnly = true) public LectureDetailResponseDto readLectureDetail(Long id) { - Lecture lecture = lectureCRUDService.loadLectureFromId(id); + Lecture lecture = findLectureById(id); return new LectureDetailResponseDto(lecture); } + public NoOffsetPaginationResponse findPagedLecturesWithSchedule( + Long cursorId, + int limit, + String keyword, + String major, + Integer grade + ) { + Slice lectureSlice = lectureRepository + .findCurrentSemesterLectures(cursorId, limit, keyword, major, grade); + + Slice result = lectureSlice + .map(this::convertLectureWithSchedule); + + return NoOffsetPaginationResponse.of(result); + } + + private LectureWithScheduleResponse convertLectureWithSchedule(Lecture lecture) { + LectureWithScheduleResponse response = LectureWithScheduleResponse.of(lecture); + String placeSchedule = lecture.getLectureDetail().getPlaceSchedule(); + + List scheduleList = LectureStringConverter + .convertScheduleChunkIntoTimetableCellScheduleList(placeSchedule); + + scheduleList.forEach(it -> response.addOriginalCellResponse(OriginalLectureCellResponse.of(it))); + return response; + } + + + /** + * 공통 메서드 + */ + public Lecture findLectureById(Long id) { + return lectureRepository.findById(id) + .orElseThrow(() -> new LectureException(LECTURE_NOT_FOUND)); + } + + private LectureAndCountResponseForm readLectureByKeywordAndOption(String keyword, LectureFindOption option) { LecturesAndCountDao lectureInfo = lectureCRUDService.loadLectureByKeywordAndOption(keyword, option); return createLectureResponseForm(lectureInfo); diff --git a/src/main/java/usw/suwiki/domain/lecture/util/LectureStringConverter.java b/src/main/java/usw/suwiki/domain/lecture/util/LectureStringConverter.java new file mode 100644 index 00000000..1b4d9ccd --- /dev/null +++ b/src/main/java/usw/suwiki/domain/lecture/util/LectureStringConverter.java @@ -0,0 +1,122 @@ +package usw.suwiki.domain.lecture.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import usw.suwiki.domain.timetable.entity.TimetableCellSchedule; +import usw.suwiki.domain.timetable.entity.TimetableDay; + +// TODO refactor: TimetableCellSchedule 의존성 제거. 해당 클래스는 스트링 변환 작업만 책임지도록 +public final class LectureStringConverter { + + /* + 변환에 필요한 최소한의 스트링 형식입니다. https://regexr.com/7q2nv + pass : "강의실107-1(수6,7,8)" "강의실 B215(화5,6,7 수5,6,7)" + fail : "(월1,2)" "강의실(1,2)" "강의실 월1,2" "강의실107(요일아님6,7,8)" + */ + private static final String PLACE_SCHEDULE_REGEX = "^([\\s가-힣A-Za-z\\d-]+\\([월화수목금토일]\\d+(?:,\\d+)*.*?\\))+$"; + + /** + * @param scheduleChunk 강의 장소 및 시간 원본 lecture.place_schedule + * @implNote place_schedule을 TimetableCellSchedule 객체 리스트로 변환 + */ + public static List convertScheduleChunkIntoTimetableCellScheduleList(String scheduleChunk) { + List scheduleList = new ArrayList<>(); + + // TODO refactor: "null" -> null. 데이터 파싱 로직 변경 필요 + if (Objects.equals(scheduleChunk, "null") + || Objects.isNull(scheduleChunk) + || !Pattern.matches(PLACE_SCHEDULE_REGEX, scheduleChunk) + ) { + return scheduleList; + } + + List locationAndDaysChunkList = splitScheduleChunkIntoLocationAndDaysChunkList(scheduleChunk); + + for (String locationAndDaysChunk : locationAndDaysChunkList) { + String location = extractLocationFromLocationAndDaysChunk(locationAndDaysChunk); + + String DaysChunk = extractDaysChunkFromLocationAndDaysElementChunk(locationAndDaysChunk); + for (String dayAndPeriodsChunk : splitDaysChunkIntoDayAndPeriodsChunkList(DaysChunk)) { + + String day = dayAndPeriodsChunk.substring(0, 1); + String periodsChunk = dayAndPeriodsChunk.substring(1); + + for (String connectedPeriods : splitUnconnectedPeriodsIntoConnectedPeriodsList(periodsChunk)) { + List periodList = convertStringListToIntList(connectedPeriods); + Integer startPeriod = periodList.get(0); + Integer endPeriod = periodList.get(periodList.size() - 1); + + TimetableCellSchedule schedule = TimetableCellSchedule.builder() + .location(location) + .day(TimetableDay.ofKorean(day)) + .startPeriod(startPeriod) + .endPeriod(endPeriod) + .build(); + scheduleList.add(schedule); + } + } + } + return scheduleList; + } + + + private static List splitScheduleChunkIntoLocationAndDaysChunkList(String chunk) { + // e.g. "IT103(..),IT505(..)" -> [ "IT103(..)", "IT505(..)" ] + return Arrays.asList(chunk.split(",(?![^()]*\\))")); + } + + private static String extractLocationFromLocationAndDaysChunk(String chunk) { + // e.g. "IT103(월1,2, 화1,2)" -> "IT103" + return chunk.split("\\(")[0]; + } + + private static String extractDaysChunkFromLocationAndDaysElementChunk(String chunk) { + // e.g. IT103(월1,2, 화1,2) -> "월1,2, 화1,2" + int start = chunk.indexOf('(') + 1, end = chunk.lastIndexOf(')'); + return chunk.substring(start, end); + } + + private static List splitDaysChunkIntoDayAndPeriodsChunkList(String chunk) { + // e.g. "월1,2, 화1,2" -> [ "월1,2", "화1,2" ] + return Arrays.asList(chunk.split(" ")); + } + + private static List splitUnconnectedPeriodsIntoConnectedPeriodsList(String unconnectedPeriods) { + // e.g. "1,2,3,5,6,7,9,10" -> [ "1,2,3", "5,6,7", "9,10" ] + String[] numbers = unconnectedPeriods.split(","); + Arrays.sort(numbers); + + List connectedPeriodsList = new ArrayList<>(); + List currentGroup = new ArrayList<>(); + + for (String number : numbers) { + int num = Integer.parseInt(number); + + if (currentGroup.isEmpty() || num == Integer.parseInt(currentGroup.get(currentGroup.size() - 1)) + 1) { + currentGroup.add(number); + } else { + connectedPeriodsList.add(String.join(",", currentGroup)); + currentGroup.clear(); + currentGroup.add(number); + } + } + + if (!currentGroup.isEmpty()) { + connectedPeriodsList.add(String.join(",", currentGroup)); + } + + return connectedPeriodsList; + } + + private static List convertStringListToIntList(String stringList) { + String[] elements = stringList.split(","); + + return Arrays.stream(elements) + .map(Integer::parseInt) + .toList(); + } + +} diff --git a/src/main/java/usw/suwiki/domain/timetable/entity/TimetableDay.java b/src/main/java/usw/suwiki/domain/timetable/entity/TimetableDay.java index 00df19e4..5b670298 100644 --- a/src/main/java/usw/suwiki/domain/timetable/entity/TimetableDay.java +++ b/src/main/java/usw/suwiki/domain/timetable/entity/TimetableDay.java @@ -8,8 +8,14 @@ @RequiredArgsConstructor public enum TimetableDay implements KeyValueEnumModel { - MON("MON"), TUE("TUE"), WED("WED"), THU("THU"), FRI("FRI"), SAT("SAT"), E_LEARNING("E_LEARNING") - ; + MON("MON"), + TUE("TUE"), + WED("WED"), + THU("THU"), + FRI("FRI"), + SAT("SAT"), + SUN("SUN"), + E_LEARNING("E_LEARNING"); private final String value; @@ -20,6 +26,20 @@ public static TimetableDay ofString(String param) { .orElseThrow(() -> new TimetableException(ExceptionType.INVALID_TIMETABLE_CELL_DAY)); } + public static TimetableDay ofKorean(String param) { + return switch (param) { + case "월" -> MON; + case "화" -> TUE; + case "수" -> WED; + case "목" -> THU; + case "금" -> FRI; + case "토" -> SAT; + case "일" -> SUN; + + default -> throw new TimetableException(ExceptionType.INVALID_TIMETABLE_CELL_DAY); + }; + } + @Override public String getKey() { return this.name(); diff --git a/src/main/java/usw/suwiki/global/dto/ApiResponse.java b/src/main/java/usw/suwiki/global/dto/ApiResponse.java index f907862c..ab710ffe 100644 --- a/src/main/java/usw/suwiki/global/dto/ApiResponse.java +++ b/src/main/java/usw/suwiki/global/dto/ApiResponse.java @@ -31,8 +31,6 @@ public static ApiResponse success(T data) { public static ApiResponse error(String code, String message) { HashMap empty = new HashMap<>(); - System.out.println("code = " + code); - System.out.println("message = " + message); return ApiResponse.builder() .code(code) .data(empty) diff --git a/src/main/java/usw/suwiki/global/dto/NoOffsetPaginationResponse.java b/src/main/java/usw/suwiki/global/dto/NoOffsetPaginationResponse.java new file mode 100644 index 00000000..fc8897cc --- /dev/null +++ b/src/main/java/usw/suwiki/global/dto/NoOffsetPaginationResponse.java @@ -0,0 +1,31 @@ +package usw.suwiki.global.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; + +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class NoOffsetPaginationResponse { + Boolean isLast; + List content; + + public static NoOffsetPaginationResponse of(Slice slice) { + return NoOffsetPaginationResponse.builder() + .content(slice.getContent()) + .isLast(slice.isLast()) + .build(); + } + + public static NoOffsetPaginationResponse of(Page page) { + return NoOffsetPaginationResponse.builder() + .content(page.getContent()) + .isLast(page.isLast()) + .build(); + } +} diff --git a/src/main/java/usw/suwiki/global/exception/ExceptionType.java b/src/main/java/usw/suwiki/global/exception/ExceptionType.java index 390c790b..0f0f74c5 100644 --- a/src/main/java/usw/suwiki/global/exception/ExceptionType.java +++ b/src/main/java/usw/suwiki/global/exception/ExceptionType.java @@ -79,6 +79,7 @@ public enum ExceptionType { NOT_EXISTS_LECTURE_NAME("LECTURE001", "강의 제목을 입력해주세요", BAD_REQUEST), NOT_EXISTS_PROFESSOR_NAME("LECTURE002", "교수 이름을 입력해주세요", BAD_REQUEST), NOT_EXISTS_LECTURE("LECTURE003", "해당 강의가 존재하지 않습니다.", BAD_REQUEST), + INVALID_LECTURE_PLACE_SCHEDULE_FORM("LECTURE101", "유효하지 않은 강의의 장소 및 교시입니다.", INTERNAL_SERVER_ERROR), //404 LECTURE_NOT_FOUND("LECTURE001", "해당 강의에 대한 정보를 찾을 수 없습니다.", NOT_FOUND), diff --git a/src/main/java/usw/suwiki/global/jwt/JwtAgent.java b/src/main/java/usw/suwiki/global/jwt/JwtAgent.java index 8c7df17c..2ffa1c2a 100644 --- a/src/main/java/usw/suwiki/global/jwt/JwtAgent.java +++ b/src/main/java/usw/suwiki/global/jwt/JwtAgent.java @@ -178,17 +178,10 @@ private static boolean isRefreshTokenNotExpired(Date date) { // 기존 만료 .atZone(ZoneId.systemDefault()) .toLocalDateTime(); - System.out.println("tokenExpiredAt = " + tokenExpiredAt); - System.out.println("LocalDateTime.now() = " + LocalDateTime.now()); - // 만료 시간 > 현재 시간 => 정상 // 현재시간 - 7일(초단위) 를 한 피연산자 할당 LocalDateTime subDetractedDateTime = LocalDateTime.now().plusSeconds(604800); - System.out.println("subDetractedDateTime = " + subDetractedDateTime); - - System.out.println( - "tokenExpiredAt.isBefore(subDetractedDateTime) = " + tokenExpiredAt.isBefore(subDetractedDateTime)); // 피연산자 보다 이전 이면 True 반환 및 갱신해줘야함 return tokenExpiredAt.isBefore(LocalDateTime.now()); } diff --git a/src/main/java/usw/suwiki/global/util/query/SlicePaginationUtils.java b/src/main/java/usw/suwiki/global/util/query/SlicePaginationUtils.java new file mode 100644 index 00000000..0489b021 --- /dev/null +++ b/src/main/java/usw/suwiki/global/util/query/SlicePaginationUtils.java @@ -0,0 +1,25 @@ +package usw.suwiki.global.util.query; + +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +public class SlicePaginationUtils { + public static final int SLICE_LIMIT_PLUS_AMOUNT = 1; + + public static Slice buildSlice(List content, int size) { + boolean hasNext = false; + + if (content.size() > size) { + content.remove(size); + hasNext = true; + } + + return new SliceImpl<>(content, Pageable.ofSize(size), hasNext); + } + + public static int increaseSliceLimit(int size) { + return size + SLICE_LIMIT_PLUS_AMOUNT; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 259b8114..d2bd479f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,4 +6,7 @@ spring: logging: level: org.hibernate.SQL: DEBUG - org.hibernate.type.descriptor.sql.BasicBinder: TRACE \ No newline at end of file + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + +business: + current-semester: 2023-2 \ No newline at end of file diff --git a/src/test/java/usw/suwiki/repository/lecture/LectureRepositoryTest.java b/src/test/java/usw/suwiki/repository/lecture/LectureRepositoryTest.java index 13d9b5f0..1bfde1eb 100644 --- a/src/test/java/usw/suwiki/repository/lecture/LectureRepositoryTest.java +++ b/src/test/java/usw/suwiki/repository/lecture/LectureRepositoryTest.java @@ -5,17 +5,23 @@ import java.util.Optional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Slice; import usw.suwiki.config.TestJpaConfig; import usw.suwiki.domain.evaluatepost.domain.EvaluatePost; import usw.suwiki.domain.lecture.domain.Lecture; +import usw.suwiki.domain.lecture.domain.LectureDetail; import usw.suwiki.domain.lecture.domain.repository.LectureRepository; import usw.suwiki.domain.user.user.User; import usw.suwiki.domain.user.user.repository.UserRepository; @@ -23,9 +29,12 @@ import usw.suwiki.template.lecture.LectureTemplate; import usw.suwiki.template.user.UserTemplate; +// TODO: RepositoryTest 슈퍼 클래스로 공통 설정 상속 +// TODO refactor: 테스트 독립성 보장. TRUNCATE 실행 @DataJpaTest @Import(TestJpaConfig.class) @TestMethodOrder(MethodOrderer.DisplayName.class) +@AutoConfigureTestDatabase(replace = Replace.NONE) public class LectureRepositoryTest { // TODO: https://7357.tistory.com/339 보면서 동시성 테스트하는 방법 공부 @Autowired private LectureRepository lectureRepository; @@ -40,17 +49,87 @@ public class LectureRepositoryTest { // TODO: https://7357.tistory.com/339 private Lecture dummyLecture; private EvaluatePost dummyEvaluatePost; + @Value("${business.current-semester}") + private String currentSemester; + @BeforeEach void setUp() { this.dummyUser = userRepository.save(UserTemplate.createDummyUser()); this.dummyLecture = lectureRepository.save(LectureTemplate.createFirstDummyLecture()); this.dummyEvaluatePost = EvaluatePostTemplate.createFirstDummyEvaluatePost(dummyUser, dummyLecture); + LectureDetail firstGradeLectureDetail = LectureDetail.builder() + .grade(1) + .evaluateType("상대평가") + .point(2.0) + .build(); + LectureDetail secondGradeLectureDetail = LectureDetail.builder() + .grade(2) + .evaluateType("상대평가") + .point(2.0) + .build(); + + /* 테스트 시나리오 (강의 리스트 조회 - 시간표상 이번 학기에 열린 강의 리스트 조회) + + 총 과목 개수는 21개 + semester: 이번 학기인 과목은 80개 + name: 이번 학기중 "도전과 창조"가 포함된 과목은 60개 + major: 이번 학기중 "교양"인 과목은 40개, "교양(야)"인 과목은 10개 + grade: 이번 학기중 2학년 과목은 20개 + */ + for (int i = 0; i < 20; i++) { + lectureRepository.save(LectureTemplate.createDummyLecture( + "2021-2, 2022-2, " + currentSemester, + "도전과 창조", + "중핵", + "교양 아님", + "우문균", + firstGradeLectureDetail + )); + lectureRepository.save(LectureTemplate.createDummyLecture( + "2021-2, 2022-2, " + currentSemester, + "테스트학개론", + "중핵", + "교양 아님", + "우문균", + firstGradeLectureDetail + )); + lectureRepository.save(LectureTemplate.createDummyLecture( + "2021-2, 2022-2, " + currentSemester, + "도전과 창조", + "중핵", + "교양", + "우문균", + firstGradeLectureDetail + )); + lectureRepository.save(LectureTemplate.createDummyLecture( + "2021-2, 2022-2, " + currentSemester, + "도전과 창조", + "중핵", + "교양", + "우문균", + secondGradeLectureDetail + )); + } + + Slice result = lectureRepository.findCurrentSemesterLectures( + 0L, + 20, + null, null, null + ); + entityManager.clear(); + } + + @AfterEach + void tearDown() { + userRepository.deleteAll(); + lectureRepository.deleteAll(); + entityManager.clear(); } @Test - @DisplayName("Lecture 단일 조회 성공") + @DisplayName("강의 단일 조회") public void selectLecture_success() { // given Long id = dummyLecture.getId(); @@ -67,7 +146,7 @@ public void selectLecture_success() { } @Test - @DisplayName("Lecture 단일 조회 성공 - 비관적 락 조회") + @DisplayName("강의 단일 조회 - 비관적 락") public void selectLecture_success_with_pessimistic_lock() { // given Long id = dummyLecture.getId(); @@ -82,4 +161,52 @@ public void selectLecture_success_with_pessimistic_lock() { assertThat(foundLecture.get().getLectureDetail().getCode()) .isEqualTo(dummyLecture.getLectureDetail().getCode()); } + + @Test + @DisplayName("시간표 강의 리스트 조회 - 이번 학기에 열린 강의 리스트 조회") + public void selectTimetableLectureList_success() { + // given + long cursorId = 0; + int limit = 80; + String keyword = "도전"; + String majorType = "교양"; + int grade = 2; + + // TODO fix: Slice 0 containing UNKNOWN instances + // when + Slice currentSemester = lectureRepository.findCurrentSemesterLectures( + cursorId, + limit, + null, + null, + null + ); + Slice keywordResult = lectureRepository.findCurrentSemesterLectures( + cursorId, + limit, + keyword, + null, + null + ); + Slice majorResult = lectureRepository.findCurrentSemesterLectures( + cursorId, + limit, + null, + majorType, + null + ); + Slice majorGradeResult = lectureRepository.findCurrentSemesterLectures( + cursorId, + limit, + null, + majorType, + grade + ); + + // then + assertThat(currentSemester.getContent().size()).isEqualTo(80); + assertThat(keywordResult.getContent().size()).isEqualTo(60); + assertThat(majorResult.getContent().size()).isEqualTo(40); + assertThat(majorGradeResult.getContent().size()).isEqualTo(20); + } } diff --git a/src/test/java/usw/suwiki/service/lecture/LectureServiceTest.java b/src/test/java/usw/suwiki/service/lecture/LectureServiceTest.java new file mode 100644 index 00000000..1c05ad73 --- /dev/null +++ b/src/test/java/usw/suwiki/service/lecture/LectureServiceTest.java @@ -0,0 +1,108 @@ +package usw.suwiki.service.lecture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import usw.suwiki.domain.lecture.controller.dto.LectureWithScheduleResponse; +import usw.suwiki.domain.lecture.domain.Lecture; +import usw.suwiki.domain.lecture.domain.repository.LectureRepository; +import usw.suwiki.domain.lecture.service.LectureService; +import usw.suwiki.global.dto.NoOffsetPaginationResponse; +import usw.suwiki.template.lecture.LectureDetailTemplate; +import usw.suwiki.template.lecture.LectureTemplate; + +@ExtendWith(MockitoExtension.class) +@TestMethodOrder(MethodOrderer.DisplayName.class) +public class LectureServiceTest { + @InjectMocks + LectureService lectureService; + + @Mock + LectureRepository lectureRepository; + + @Value("${business.current-semester}") + private String currentSemester; + + public static final long SPYING_ID = 123712L; + + @Test + @DisplayName("시간표 생성시 강의 검색 - 이번 학기에 열린 강의 리스트 조회") + public void SELECT_LECTURE_LIST_TIMETABLE_THIS_SEMESTER() { + // given + Lecture multipleLocationLecture = spy(LectureTemplate.createDummyLecture( + currentSemester, + "물리학및실험2", + "전교", + "전자재료공학부", + "전계진", + LectureDetailTemplate.varyPlaceSchedule("미래103(화1,2),미래B102(화3,4)") // 스케줄 2개 + )); + Lecture multipleDayLecture = spy(LectureTemplate.createDummyLecture( + currentSemester, + "관현악합주6", + "전핵", + "관현악과", + "박태영", + LectureDetailTemplate.varyPlaceSchedule("음악109(월5,6 화5,6 수5,6)") // 스케줄 3개 + )); + Lecture multipleLocationAndDayLecture = spy(LectureTemplate.createDummyLecture( + currentSemester, + "역대급헬강의", + "전핵", + "역대급학부", + "역대급교수", + LectureDetailTemplate.varyPlaceSchedule("미래103(월1,2 화1,2),미래B102(월7,8 화7,8)") // 스케줄 4개 + )); + Lecture unconnectedPeriodsLecture = spy(LectureTemplate.createDummyLecture( + currentSemester, + "건축설계2", + "전핵", + "건축학", + "최기원", + LectureDetailTemplate.varyPlaceSchedule("2공학207(목1,2,3,5,6,7)") // 스케줄 2개 + )); + + List lectureList = List.of( + multipleLocationLecture, + multipleDayLecture, + multipleLocationAndDayLecture, + unconnectedPeriodsLecture + ); + Slice queryResult = new SliceImpl<>(lectureList); + given(lectureRepository.findCurrentSemesterLectures(anyLong(), anyInt(), any(), any(), any())) + .willReturn(queryResult); + + when(multipleLocationLecture.getId()).thenReturn(SPYING_ID); + when(multipleDayLecture.getId()).thenReturn(SPYING_ID); + when(multipleLocationAndDayLecture.getId()).thenReturn(SPYING_ID); + when(unconnectedPeriodsLecture.getId()).thenReturn(SPYING_ID); + + // when + NoOffsetPaginationResponse response = lectureService + .findPagedLecturesWithSchedule(0L, 20, null, null, null); + + // then + assertThat(response.getIsLast()).isTrue(); + assertThat(response.getContent().get(0).getOriginalCellList().size()).isEqualTo(2); + assertThat(response.getContent().get(1).getOriginalCellList().size()).isEqualTo(3); + assertThat(response.getContent().get(2).getOriginalCellList().size()).isEqualTo(4); + assertThat(response.getContent().get(3).getOriginalCellList().size()).isEqualTo(2); + } +} diff --git a/src/test/java/usw/suwiki/template/lecture/LectureDetailTemplate.java b/src/test/java/usw/suwiki/template/lecture/LectureDetailTemplate.java index db8fd38e..833c8839 100644 --- a/src/test/java/usw/suwiki/template/lecture/LectureDetailTemplate.java +++ b/src/test/java/usw/suwiki/template/lecture/LectureDetailTemplate.java @@ -11,11 +11,15 @@ public class LectureDetailTemplate { public static final int GRADE = 2; public static final String EVALUATE_TYPE = "상대평가"; - public static LectureDetail createfirstDummyLectureDetail() { - return createDummyLectureDetail(PLACE_SCHEDULE, CODE, POINT, CAPPR_TYPE, DICL_NO, GRADE, EVALUATE_TYPE); + public static LectureDetail createFirstDummy() { + return createDummy(PLACE_SCHEDULE, CODE, POINT, CAPPR_TYPE, DICL_NO, GRADE, EVALUATE_TYPE); } - public static LectureDetail createDummyLectureDetail( + public static LectureDetail varyPlaceSchedule(String placeSchedule) { + return createDummy(placeSchedule, CODE, POINT, CAPPR_TYPE, DICL_NO, GRADE, EVALUATE_TYPE); + } + + public static LectureDetail createDummy( String placeSchedule, String code, double point, diff --git a/src/test/java/usw/suwiki/template/lecture/LectureTemplate.java b/src/test/java/usw/suwiki/template/lecture/LectureTemplate.java index e9dac985..2ac37fe3 100644 --- a/src/test/java/usw/suwiki/template/lecture/LectureTemplate.java +++ b/src/test/java/usw/suwiki/template/lecture/LectureTemplate.java @@ -4,12 +4,12 @@ import usw.suwiki.domain.lecture.domain.LectureDetail; public class LectureTemplate { - public static final String SEMESTER = "2022-2, 2023-2"; + public static final String SEMESTER = "2022-1, 2023-1"; public static final String PROFESSOR = "신호진"; public static final String NAME = "데이터 구조"; public static final String MAJOR_TYPE = "컴퓨터 학부"; public static final String LECTURE_TYPE = "전핵"; - public static final LectureDetail LECTURE_DETAIL = LectureDetailTemplate.createfirstDummyLectureDetail(); + public static final LectureDetail LECTURE_DETAIL = LectureDetailTemplate.createFirstDummy(); public static Lecture createFirstDummyLecture() { return createDummyLecture(SEMESTER, PROFESSOR, NAME, MAJOR_TYPE, LECTURE_TYPE, LECTURE_DETAIL); @@ -17,18 +17,18 @@ public static Lecture createFirstDummyLecture() { public static Lecture createDummyLecture( String semester, - String professor, String name, - String majorType, String type, + String majorType, + String professor, LectureDetail lectureDetail ) { return Lecture.builder() .semester(semester) - .professor(professor) .name(name) - .majorType(majorType) .type(type) + .majorType(majorType) + .professor(professor) .lectureDetail(lectureDetail) .build(); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 00000000..00284aa6 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,24 @@ +spring: + + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:test;MODE=MySQL + username: sa + password: + + jpa: + database: h2 + generate-ddl: true + open-in-view: false + hibernate: + ddl-auto: create-drop + properties: + dialect: org.hibernate.dialect.MySQL57InnoDBDialect + hibernate: + format_sql: true +# SQL 예약어 예외 방지 (user, year) + globally_quoted_identifiers: true + globally_quoted_identifiers_skip_column_definitions : true + +business: + current-semester: 2023-2