diff --git a/build.gradle b/build.gradle index 74b34945..a6ed1626 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" implementation "com.querydsl:querydsl-apt:${queryDslVersion}" + testImplementation 'junit:junit:4.13.1' + testImplementation 'junit:junit:4.13.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' @@ -48,6 +50,9 @@ dependencies { // swagger implementation 'io.springfox:springfox-boot-starter:3.0.0' implementation 'io.springfox:springfox-swagger-ui:3.0.0' + + // gson + implementation 'com.google.code.gson:gson:2.8.9' } tasks.named('test') { diff --git a/src/main/java/org/sopt/makers/operation/config/GenerationConfig.java b/src/main/java/org/sopt/makers/operation/config/GenerationConfig.java deleted file mode 100644 index 890de985..00000000 --- a/src/main/java/org/sopt/makers/operation/config/GenerationConfig.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.sopt.makers.operation.config; - -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - - -@Getter -@Configuration -public class GenerationConfig { - @Value("${sopt.current.generation}") - private int currentGeneration; -} diff --git a/src/main/java/org/sopt/makers/operation/config/ValueConfig.java b/src/main/java/org/sopt/makers/operation/config/ValueConfig.java new file mode 100644 index 00000000..e38673ff --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/config/ValueConfig.java @@ -0,0 +1,23 @@ +package org.sopt.makers.operation.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; + +@Configuration +@Getter +public class ValueConfig { + + @Value("${sopt.alarm.message.title_end}") + private String ALARM_MESSAGE_TITLE; + @Value("${sopt.alarm.message.content_end}") + private String ALARM_MESSAGE_CONTENT; + @Value("${sopt.current.generation}") + private int GENERATION; + + private final int SUB_LECTURE_MAX_ROUND = 2; + private final String ETC_MESSAGE = "출석 점수가 반영되지 않아요."; + private final String SEMINAR_MESSAGE = ""; + private final String EVENT_MESSAGE = "행사도 참여하고, 출석점수도 받고, 일석이조!"; +} diff --git a/src/main/java/org/sopt/makers/operation/controller/ScheduleController.java b/src/main/java/org/sopt/makers/operation/controller/ScheduleController.java index 8ede0853..5b7a673e 100644 --- a/src/main/java/org/sopt/makers/operation/controller/ScheduleController.java +++ b/src/main/java/org/sopt/makers/operation/controller/ScheduleController.java @@ -16,7 +16,7 @@ public class ScheduleController { @Scheduled(cron = "0 0 0 ? * SUN") public void endLecture() { - lectureService.finishLecture(); + lectureService.endLectures(); } } diff --git a/src/main/java/org/sopt/makers/operation/controller/app/AppLectureController.java b/src/main/java/org/sopt/makers/operation/controller/app/AppLectureController.java index 663aa109..309af9e0 100644 --- a/src/main/java/org/sopt/makers/operation/controller/app/AppLectureController.java +++ b/src/main/java/org/sopt/makers/operation/controller/app/AppLectureController.java @@ -1,7 +1,5 @@ package org.sopt.makers.operation.controller.app; - -import static java.util.Objects.*; import static org.sopt.makers.operation.common.ResponseMessage.*; import io.swagger.annotations.ApiOperation; @@ -19,11 +17,14 @@ @RequiredArgsConstructor @RequestMapping("/api/v1/app/lectures") public class AppLectureController { + private final LectureService lectureService; - @ApiOperation(value = "단일 세미나 상태 조회") + + @ApiOperation(value = "진행 중인 세미나 상태 조회") @GetMapping public ResponseEntity getLecture(@ApiIgnore Principal principal) { - val response = lectureService.getCurrentLecture(getMemberId(principal)); + val memberPlaygroundId = Long.parseLong(principal.getName()); + val response = lectureService.getTodayLecture(memberPlaygroundId); return ResponseEntity.ok(ApiResponse.success(SUCCESS_SINGLE_GET_LECTURE.getMessage(), response)); } @@ -33,8 +34,4 @@ public ResponseEntity getRound(@PathVariable("lectureId") Long lect val response = lectureService.getCurrentLectureRound(lectureId); return ResponseEntity.ok(ApiResponse.success(SUCCESS_GET_LECTURE_ROUND.getMessage(), response)); } - - private Long getMemberId(Principal principal) { - return nonNull(principal) ? Long.valueOf(principal.getName()) : null; - } } diff --git a/src/main/java/org/sopt/makers/operation/controller/web/LectureController.java b/src/main/java/org/sopt/makers/operation/controller/web/LectureController.java index 1fdd49db..156cc045 100644 --- a/src/main/java/org/sopt/makers/operation/controller/web/LectureController.java +++ b/src/main/java/org/sopt/makers/operation/controller/web/LectureController.java @@ -1,5 +1,6 @@ package org.sopt.makers.operation.controller.web; +import static org.sopt.makers.operation.common.ApiResponse.*; import static org.sopt.makers.operation.common.ResponseMessage.*; import java.net.URI; @@ -39,22 +40,21 @@ public ResponseEntity createLecture(@RequestBody LectureRequestDTO val lectureId = lectureService.createLecture(requestDTO); return ResponseEntity .created(getURI(lectureId)) - .body(ApiResponse.success(SUCCESS_CREATE_LECTURE.getMessage(), lectureId)); + .body(success(SUCCESS_CREATE_LECTURE.getMessage(), lectureId)); } @ApiOperation(value = "세션 리스트 조회") @GetMapping - public ResponseEntity getLecturesByGeneration( - @RequestParam("generation") int generation, @RequestParam(required = false) Part part) { - val response = lectureService.getLecturesByGeneration(generation, part); - return ResponseEntity.ok(ApiResponse.success(SUCCESS_GET_LECTURES.getMessage(), response)); + public ResponseEntity getLectures(@RequestParam int generation, @RequestParam(required = false) Part part) { + val response = lectureService.getLectures(generation, part); + return ResponseEntity.ok(success(SUCCESS_GET_LECTURES.getMessage(), response)); } - @ApiOperation(value = "세션 상세 조회") + @ApiOperation(value = "세션 단일 조회") @GetMapping("/{lectureId}") public ResponseEntity getLecture(@PathVariable Long lectureId) { val response = lectureService.getLecture(lectureId); - return ResponseEntity.ok(ApiResponse.success(SUCCESS_GET_LECTURE.getMessage(), response)); + return ResponseEntity.ok(success(SUCCESS_GET_LECTURE.getMessage(), response)); } @ApiOperation(value = "출석 시작") @@ -63,35 +63,35 @@ public ResponseEntity startAttendance(@RequestBody AttendanceReques val response = lectureService.startAttendance(requestDTO); return ResponseEntity .created(getURI(requestDTO.lectureId())) - .body(ApiResponse.success(SUCCESS_START_ATTENDANCE.getMessage(), response)); + .body(success(SUCCESS_START_ATTENDANCE.getMessage(), response)); } - @ApiOperation(value = "출석 점수 갱신 트리거 (출석 종료)") + private URI getURI(Long lectureId) { + return ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{lectureId}") + .buildAndExpand(lectureId) + .toUri(); + } + + @ApiOperation(value = "세션 종료 후 출석 점수 갱신") @PatchMapping("/{lectureId}") - public ResponseEntity finishLecture(@PathVariable("lectureId") Long lectureId) { - lectureService.finishLecture(lectureId); - return ResponseEntity.ok(ApiResponse.success(SUCCESS_UPDATE_MEMBER_SCORE.getMessage())); + public ResponseEntity endLecture(@PathVariable Long lectureId) { + lectureService.endLecture(lectureId); + return ResponseEntity.ok(success(SUCCESS_UPDATE_MEMBER_SCORE.getMessage())); } @ApiOperation(value = "세션 삭제") @DeleteMapping("/{lectureId}") public ResponseEntity deleteLecture(@PathVariable Long lectureId) { lectureService.deleteLecture(lectureId); - return ResponseEntity.ok(ApiResponse.success(SUCCESS_DELETE_LECTURE.getMessage())); + return ResponseEntity.ok(success(SUCCESS_DELETE_LECTURE.getMessage())); } - @ApiOperation(value = "세션 상세 조회 (팝업)") + @ApiOperation(value = "세션 팝업용 상세 조회") @GetMapping("/detail/{lectureId}") public ResponseEntity getLectureDetail(@PathVariable Long lectureId) { val response = lectureService.getLectureDetail(lectureId); - return ResponseEntity.ok(ApiResponse.success(SUCCESS_GET_LECTURE.getMessage(), response)); - } - - private URI getURI(Long lectureId) { - return ServletUriComponentsBuilder - .fromCurrentRequest() - .path("/{lectureId}") - .buildAndExpand(lectureId) - .toUri(); + return ResponseEntity.ok(success(SUCCESS_GET_LECTURE.getMessage(), response)); } } diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/AttendanceResponseDTO.java b/src/main/java/org/sopt/makers/operation/dto/lecture/AttendanceResponseDTO.java index 9376a41a..8e61faf9 100644 --- a/src/main/java/org/sopt/makers/operation/dto/lecture/AttendanceResponseDTO.java +++ b/src/main/java/org/sopt/makers/operation/dto/lecture/AttendanceResponseDTO.java @@ -1,4 +1,20 @@ package org.sopt.makers.operation.dto.lecture; -public record AttendanceResponseDTO(Long lectureId, Long subLectureId) { +import org.sopt.makers.operation.entity.SubLecture; +import org.sopt.makers.operation.entity.lecture.Lecture; + +import lombok.Builder; + +@Builder +public record AttendanceResponseDTO( + Long lectureId, + Long subLectureId +) { + + public static AttendanceResponseDTO of(Lecture lecture, SubLecture subLecture) { + return AttendanceResponseDTO.builder() + .lectureId(lecture.getId()) + .subLectureId(subLecture.getId()) + .build(); + } } diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureRequestDTO.java b/src/main/java/org/sopt/makers/operation/dto/lecture/LectureRequestDTO.java index 970f1f76..96c99649 100644 --- a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureRequestDTO.java +++ b/src/main/java/org/sopt/makers/operation/dto/lecture/LectureRequestDTO.java @@ -9,6 +9,7 @@ import lombok.*; +@Builder public record LectureRequestDTO( @NonNull Part part, @NonNull String name, diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureResponseDTO.java b/src/main/java/org/sopt/makers/operation/dto/lecture/LectureResponseDTO.java index 69e90313..81a07c6b 100644 --- a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureResponseDTO.java +++ b/src/main/java/org/sopt/makers/operation/dto/lecture/LectureResponseDTO.java @@ -1,6 +1,8 @@ package org.sopt.makers.operation.dto.lecture; import static java.util.Objects.*; + +import java.time.LocalDateTime; import java.util.List; import org.sopt.makers.operation.entity.Part; @@ -35,16 +37,25 @@ public static LectureResponseDTO of(Lecture lecture) { .status(lecture.getLectureStatus()) .build(); } -} -record SubLectureVO( - Long subLectureId, - int round, - String startAt, - String code -) { - static SubLectureVO of(SubLecture subLecture) { - val startAt = nonNull(subLecture.getStartAt()) ? subLecture.getStartAt().toString() : null; - return new SubLectureVO(subLecture.getId(), subLecture.getRound(), startAt, subLecture.getCode()); + @Builder + public record SubLectureVO( + Long subLectureId, + int round, + String startAt, + String code + ) { + private static SubLectureVO of(SubLecture subLecture) { + return SubLectureVO.builder() + .subLectureId(subLecture.getId()) + .round(subLecture.getRound()) + .startAt(getStartAt(subLecture.getStartAt())) + .code(subLecture.getCode()) + .build(); + } + + private static String getStartAt(LocalDateTime startAt) { + return nonNull(startAt) ? startAt.toString() : null; + } } } \ No newline at end of file diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/LecturesResponseDTO.java b/src/main/java/org/sopt/makers/operation/dto/lecture/LecturesResponseDTO.java index 253a9f25..2a831d19 100644 --- a/src/main/java/org/sopt/makers/operation/dto/lecture/LecturesResponseDTO.java +++ b/src/main/java/org/sopt/makers/operation/dto/lecture/LecturesResponseDTO.java @@ -9,44 +9,44 @@ import lombok.*; public record LecturesResponseDTO( - int generation, - List lectures + int generation, + List lectures ) { public static LecturesResponseDTO of(int generation, List lectures) { return new LecturesResponseDTO( - generation, - lectures.stream().map(LectureVO::of).toList() + generation, + lectures.stream().map(LectureVO::of).toList() ); } -} -@Builder -record LectureVO( - Long lectureId, - String name, - Part partValue, - String partName, - String startDate, - String endDate, - Attribute attributeValue, - String attributeName, - String place, - AttendancesStatusVO attendances -) { - public static LectureVO of(Lecture lecture) { - return LectureVO.builder() - .lectureId(lecture.getId()) - .name(lecture.getName()) - .partValue(lecture.getPart()) - .partName(lecture.getPart().getName()) - .startDate(lecture.getStartDate().toString()) - .endDate(lecture.getEndDate().toString()) - .attributeValue(lecture.getAttribute()) - .attributeName(lecture.getAttribute().getName()) - .place(lecture.getPlace()) - .attendances(AttendancesStatusVO.of(lecture)) - .build(); + @Builder + public record LectureVO( + Long lectureId, + String name, + Part partValue, + String partName, + String startDate, + String endDate, + Attribute attributeValue, + String attributeName, + String place, + AttendancesStatusVO attendances + ) { + private static LectureVO of(Lecture lecture) { + return LectureVO.builder() + .lectureId(lecture.getId()) + .name(lecture.getName()) + .partValue(lecture.getPart()) + .partName(lecture.getPart().getName()) + .startDate(lecture.getStartDate().toString()) + .endDate(lecture.getEndDate().toString()) + .attributeValue(lecture.getAttribute()) + .attributeName(lecture.getAttribute().getName()) + .place(lecture.getPlace()) + .attendances(AttendancesStatusVO.of(lecture)) + .build(); + } } } diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureGetResponseDTO.java b/src/main/java/org/sopt/makers/operation/dto/lecture/TodayLectureResponseDTO.java similarity index 63% rename from src/main/java/org/sopt/makers/operation/dto/lecture/LectureGetResponseDTO.java rename to src/main/java/org/sopt/makers/operation/dto/lecture/TodayLectureResponseDTO.java index 7b1f9696..40685563 100644 --- a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureGetResponseDTO.java +++ b/src/main/java/org/sopt/makers/operation/dto/lecture/TodayLectureResponseDTO.java @@ -11,7 +11,7 @@ import java.util.stream.Collectors; @Builder -public record LectureGetResponseDTO( +public record TodayLectureResponseDTO( LectureResponseType type, Long id, String location, @@ -21,9 +21,9 @@ public record LectureGetResponseDTO( String message, List attendances ) { - public static LectureGetResponseDTO of(LectureResponseType type, Lecture lecture, String message, List attendances) { + public static TodayLectureResponseDTO of(LectureResponseType type, Lecture lecture, String message, List attendances) { - return LectureGetResponseDTO.builder() + return TodayLectureResponseDTO.builder() .type(type) .id(lecture.getId()) .location(lecture.getPlace()) @@ -40,22 +40,22 @@ public static LectureGetResponseDTO of(LectureResponseType type, Lecture lecture private static DateTimeFormatter convertFormat() { return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); } -} - -@Builder -record LectureGetResponseVO( - AttendanceStatus status, - String attendedAt -) { - public static LectureGetResponseVO of(AttendanceStatus status, LocalDateTime attendedAt) { - return LectureGetResponseVO.builder() - .status(status) - .attendedAt(attendedAt.format((convertFormat()))) - .build(); - } - - private static DateTimeFormatter convertFormat() { - return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + @Builder + record LectureGetResponseVO( + AttendanceStatus status, + String attendedAt + + ) { + public static LectureGetResponseVO of(AttendanceStatus status, LocalDateTime attendedAt) { + return LectureGetResponseVO.builder() + .status(status) + .attendedAt(attendedAt.format((convertFormat()))) + .build(); + } + + private static DateTimeFormatter convertFormat() { + return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + } } } diff --git a/src/main/java/org/sopt/makers/operation/entity/lecture/Lecture.java b/src/main/java/org/sopt/makers/operation/entity/lecture/Lecture.java index 29201733..8fd2b9af 100644 --- a/src/main/java/org/sopt/makers/operation/entity/lecture/Lecture.java +++ b/src/main/java/org/sopt/makers/operation/entity/lecture/Lecture.java @@ -1,6 +1,7 @@ package org.sopt.makers.operation.entity.lecture; import static javax.persistence.GenerationType.*; +import static org.sopt.makers.operation.entity.lecture.LectureStatus.*; import java.time.LocalDateTime; import java.util.ArrayList; @@ -67,15 +68,27 @@ public Lecture(String name, Part part, int generation, String place, LocalDateTi this.startDate = startDate; this.endDate = endDate; this.attribute = attribute; - this.lectureStatus = LectureStatus.BEFORE; + this.lectureStatus = BEFORE; } public void updateStatus(LectureStatus status) { this.lectureStatus = status; } - public void finish() { - this.lectureStatus = LectureStatus.END; + public void updateToEnd() { + this.lectureStatus = END; attendances.forEach(Attendance::updateMemberScore); } + + public boolean isEnd() { + return this.lectureStatus.equals(END); + } + + public boolean isBefore() { + return this.lectureStatus.equals(BEFORE); + } + + public boolean isFirst() { + return this.lectureStatus.equals(FIRST); + } } diff --git a/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceCustomRepository.java b/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceCustomRepository.java index dcbd655e..40e8f7cf 100644 --- a/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceCustomRepository.java +++ b/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceCustomRepository.java @@ -13,7 +13,6 @@ public interface AttendanceCustomRepository { List findAttendanceByMemberId(Long memberId); List findByLecture(Long lectureId, Part part, Pageable pageable); List findByMember(Member member); - List findCurrentAttendanceByMember(Long playGroundId); - List findSubAttendanceByAttendanceId(Long attendanceId); + List findToday(long memberPlaygroundId); int countByLectureIdAndPart(long lectureId, Part part); } diff --git a/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceRepositoryImpl.java b/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceRepositoryImpl.java index 497ae2f9..708592c1 100644 --- a/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceRepositoryImpl.java +++ b/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceRepositoryImpl.java @@ -8,17 +8,17 @@ import static org.sopt.makers.operation.entity.QSubLecture.*; import static org.sopt.makers.operation.entity.lecture.QLecture.*; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import lombok.val; -import org.sopt.makers.operation.config.GenerationConfig; +import org.sopt.makers.operation.config.ValueConfig; import org.sopt.makers.operation.entity.Attendance; import org.sopt.makers.operation.entity.Member; import org.sopt.makers.operation.entity.Part; -import org.sopt.makers.operation.entity.SubAttendance; import org.sopt.makers.operation.entity.lecture.LectureStatus; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @@ -33,7 +33,7 @@ public class AttendanceRepositoryImpl implements AttendanceCustomRepository { private final JPAQueryFactory queryFactory; - private final GenerationConfig generationConfig; + private final ValueConfig valueConfig; @Override public List findAttendanceByMemberId(Long memberId) { @@ -44,7 +44,7 @@ public List findAttendanceByMemberId(Long memberId) { .leftJoin(attendance.lecture, lecture) .where(attendance.member.id.eq(memberId), lecture.lectureStatus.eq(LectureStatus.END), - lecture.generation.eq(generationConfig.getCurrentGeneration()) + lecture.generation.eq(valueConfig.getGENERATION()) ) .orderBy(attendance.lecture.startDate.desc()) .fetch(); @@ -81,40 +81,25 @@ public List findByMember(Member member) { } @Override - public List findCurrentAttendanceByMember(Long playGroundId) { - val now = LocalDateTime.now(); - val today = now.toLocalDate(); + public List findToday(long memberPlaygroundId) { + val today = LocalDate.now(); val startOfDay = today.atStartOfDay(); val endOfDay = LocalDateTime.of(today, LocalTime.MAX); - return queryFactory - .select(attendance) - .from(attendance) + .selectFrom(attendance) .leftJoin(attendance.lecture, lecture).fetchJoin() .leftJoin(attendance.member, member).fetchJoin() + .leftJoin(attendance.subAttendances, subAttendance).fetchJoin().distinct() + .leftJoin(subAttendance.subLecture, subLecture).fetchJoin() .where( + member.playgroundId.eq(memberPlaygroundId), + member.generation.eq(valueConfig.getGENERATION()), lecture.part.eq(member.part).or(lecture.part.eq(Part.ALL)), - lecture.startDate.between(startOfDay, endOfDay), - member.playgroundId.eq(playGroundId), - member.generation.eq(generationConfig.getCurrentGeneration()) - ) + lecture.startDate.between(startOfDay, endOfDay)) .orderBy(lecture.startDate.asc()) .fetch(); } - @Override - public List findSubAttendanceByAttendanceId(Long attendanceId) { - return queryFactory - .select(subAttendance) - .from(subAttendance) - .leftJoin(subAttendance.subLecture, subLecture).fetchJoin() - .where( - subAttendance.attendance.id.eq(attendanceId) - ) - .orderBy(subAttendance.createdDate.asc()) - .fetch(); - } - @Override public int countByLectureIdAndPart(long lectureId, Part part) { return Math.toIntExact(queryFactory diff --git a/src/main/java/org/sopt/makers/operation/repository/lecture/LectureCustomRepository.java b/src/main/java/org/sopt/makers/operation/repository/lecture/LectureCustomRepository.java index b17ca536..64bf58ae 100644 --- a/src/main/java/org/sopt/makers/operation/repository/lecture/LectureCustomRepository.java +++ b/src/main/java/org/sopt/makers/operation/repository/lecture/LectureCustomRepository.java @@ -6,7 +6,7 @@ import java.util.Optional; public interface LectureCustomRepository { - List findLectures(int generation, Part part); + List find(int generation, Part part); List findLecturesToBeEnd(); Optional find(Long lectureId); } diff --git a/src/main/java/org/sopt/makers/operation/repository/lecture/LectureRepositoryImpl.java b/src/main/java/org/sopt/makers/operation/repository/lecture/LectureRepositoryImpl.java index 85772ee1..51960170 100644 --- a/src/main/java/org/sopt/makers/operation/repository/lecture/LectureRepositoryImpl.java +++ b/src/main/java/org/sopt/makers/operation/repository/lecture/LectureRepositoryImpl.java @@ -22,7 +22,7 @@ public class LectureRepositoryImpl implements LectureCustomRepository { private final JPAQueryFactory queryFactory; @Override - public List findLectures(int generation, Part part) { + public List find(int generation, Part part) { return queryFactory .selectFrom(lecture) .leftJoin(lecture.attendances, attendance).fetchJoin().distinct() diff --git a/src/main/java/org/sopt/makers/operation/repository/member/MemberCustomRepository.java b/src/main/java/org/sopt/makers/operation/repository/member/MemberCustomRepository.java index 9c8c2a84..e54fd4b3 100644 --- a/src/main/java/org/sopt/makers/operation/repository/member/MemberCustomRepository.java +++ b/src/main/java/org/sopt/makers/operation/repository/member/MemberCustomRepository.java @@ -13,4 +13,5 @@ public interface MemberCustomRepository { List search(MemberSearchCondition condition); Optional find(Long memberId); int countByGenerationAndPart(int generation, Part part); + List find(int generation, Part part); } diff --git a/src/main/java/org/sopt/makers/operation/repository/member/MemberRepositoryImpl.java b/src/main/java/org/sopt/makers/operation/repository/member/MemberRepositoryImpl.java index b5259490..4bfc6858 100644 --- a/src/main/java/org/sopt/makers/operation/repository/member/MemberRepositoryImpl.java +++ b/src/main/java/org/sopt/makers/operation/repository/member/MemberRepositoryImpl.java @@ -80,7 +80,20 @@ public int countByGenerationAndPart(int generation, Part part) { .fetchFirst()); } + @Override + public List find(int generation, Part part) { + StringExpression firstName = Expressions.stringTemplate("SUBSTR({0}, 1, 1)", member.name); + + return queryFactory + .selectFrom(member) + .where( + member.generation.eq(generation), + partEq(part)) + .orderBy(firstName.asc()) + .fetch(); + } + private BooleanExpression partEq(Part part) { - return nonNull(part) ? member.part.eq(part) : null; + return (isNull(part) || part.equals(ALL)) ? null : member.part.eq(part); } } diff --git a/src/main/java/org/sopt/makers/operation/service/AlarmServiceImpl.java b/src/main/java/org/sopt/makers/operation/service/AlarmServiceImpl.java index 08c79780..a08a42dc 100644 --- a/src/main/java/org/sopt/makers/operation/service/AlarmServiceImpl.java +++ b/src/main/java/org/sopt/makers/operation/service/AlarmServiceImpl.java @@ -5,6 +5,7 @@ import static org.sopt.makers.operation.entity.Part.*; import static org.sopt.makers.operation.entity.alarm.Status.*; +import org.sopt.makers.operation.config.ValueConfig; import org.sopt.makers.operation.dto.alarm.AlarmSendRequestDTO; import org.sopt.makers.operation.dto.alarm.AlarmSenderDTO; import org.sopt.makers.operation.dto.member.MemberSearchCondition; @@ -15,7 +16,6 @@ import org.sopt.makers.operation.external.api.PlayGroundServer; import org.sopt.makers.operation.repository.alarm.AlarmRepository; import org.sopt.makers.operation.repository.member.MemberRepository; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,13 +34,12 @@ @Service @RequiredArgsConstructor public class AlarmServiceImpl implements AlarmService { - @Value("${sopt.current.generation}") - private int currentGeneration; private final AlarmRepository alarmRepository; private final MemberRepository memberRepository; private final AlarmSender alarmSender; private final PlayGroundServer playGroundServer; + private final ValueConfig valueConfig; @Override @Transactional @@ -68,7 +67,7 @@ private List getTargetIdList(Alarm alarm) { return activeTargetList; } - val inactiveTargetList = getInactiveTargetList(currentGeneration, alarm.getPart()); + val inactiveTargetList = getInactiveTargetList(valueConfig.getGENERATION(), alarm.getPart()); return inactiveTargetList.stream() .filter(target -> !activeTargetList.contains(target)) .toList(); @@ -76,7 +75,7 @@ private List getTargetIdList(Alarm alarm) { private List getActiveTargetList(Part part) { part = part.equals(ALL) ? null : part; - val members = memberRepository.search(new MemberSearchCondition(part, currentGeneration)); + val members = memberRepository.search(new MemberSearchCondition(part, valueConfig.getGENERATION())); return members.stream() .filter(member -> nonNull(member.getPlaygroundId())) .map(member -> String.valueOf(member.getPlaygroundId())) diff --git a/src/main/java/org/sopt/makers/operation/service/AttendanceServiceImpl.java b/src/main/java/org/sopt/makers/operation/service/AttendanceServiceImpl.java index 1d4e6645..7ac524b1 100644 --- a/src/main/java/org/sopt/makers/operation/service/AttendanceServiceImpl.java +++ b/src/main/java/org/sopt/makers/operation/service/AttendanceServiceImpl.java @@ -3,12 +3,10 @@ import static java.util.Objects.nonNull; import static org.sopt.makers.operation.common.ExceptionMessage.*; -import java.util.List; - +import org.sopt.makers.operation.config.ValueConfig; import org.sopt.makers.operation.dto.attendance.AttendanceMemberResponseDTO; import org.sopt.makers.operation.dto.attendance.SubAttendanceUpdateRequestDTO; import org.sopt.makers.operation.dto.attendance.SubAttendanceUpdateResponseDTO; -import org.sopt.makers.operation.dto.attendance.MemberResponseDTO; import lombok.val; import org.sopt.makers.operation.dto.attendance.*; import org.sopt.makers.operation.entity.AttendanceStatus; @@ -22,7 +20,6 @@ import org.sopt.makers.operation.exception.SubLectureException; import org.sopt.makers.operation.repository.lecture.SubLectureRepository; import org.sopt.makers.operation.repository.member.MemberRepository; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,9 +39,7 @@ public class AttendanceServiceImpl implements AttendanceService { private final MemberRepository memberRepository; private final SubLectureRepository subLectureRepository; private final AttendanceRepository attendanceRepository; - - @Value("${sopt.current.generation}") - private int currentGeneration; + private final ValueConfig valueConfig; @Override @Transactional @@ -81,7 +76,7 @@ public AttendancesResponseDTO findAttendancesByLecture(Long lectureId, Part part @Transactional public AttendResponseDTO attend(Long playGroundId, AttendRequestDTO requestDTO) { log.info("[Attendance: attend start] id: " + playGroundId); - val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, currentGeneration) + val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, valueConfig.getGENERATION()) .orElseThrow(() -> new MemberException(INVALID_MEMBER.getName())); val memberId = member.getId(); diff --git a/src/main/java/org/sopt/makers/operation/service/LectureService.java b/src/main/java/org/sopt/makers/operation/service/LectureService.java index 910aadc1..b133cc73 100644 --- a/src/main/java/org/sopt/makers/operation/service/LectureService.java +++ b/src/main/java/org/sopt/makers/operation/service/LectureService.java @@ -1,6 +1,6 @@ package org.sopt.makers.operation.service; -import org.sopt.makers.operation.dto.lecture.LectureGetResponseDTO; +import org.sopt.makers.operation.dto.lecture.TodayLectureResponseDTO; import org.sopt.makers.operation.dto.lecture.AttendanceRequestDTO; import org.sopt.makers.operation.dto.lecture.AttendanceResponseDTO; import org.sopt.makers.operation.dto.lecture.LectureRequestDTO; @@ -10,15 +10,20 @@ import org.sopt.makers.operation.dto.lecture.*; public interface LectureService { - Long createLecture(LectureRequestDTO requestDTO); - LectureGetResponseDTO getCurrentLecture(Long playGroundId); - LecturesResponseDTO getLecturesByGeneration(int generation, Part part); - LectureResponseDTO getLecture(Long lectureId); + + /** WEB **/ + long createLecture(LectureRequestDTO requestDTO); + LecturesResponseDTO getLectures(int generation, Part part); + LectureResponseDTO getLecture(long lectureId); AttendanceResponseDTO startAttendance(AttendanceRequestDTO requestDTO); - void finishLecture(Long lectureId); - void finishLecture(); - LectureCurrentRoundResponseDTO getCurrentLectureRound(Long lectureId); + void endLecture(Long lectureId); void deleteLecture(Long lectureId); - LectureDetailResponseDTO getLectureDetail(Long lectureId); + LectureDetailResponseDTO getLectureDetail(long lectureId); + + /** SCHEDULER **/ + void endLectures(); + /** APP **/ + TodayLectureResponseDTO getTodayLecture(long memberPlaygroundId); + LectureCurrentRoundResponseDTO getCurrentLectureRound(long lectureId); } diff --git a/src/main/java/org/sopt/makers/operation/service/LectureServiceImpl.java b/src/main/java/org/sopt/makers/operation/service/LectureServiceImpl.java index edbc3cd3..dc4c6fcd 100644 --- a/src/main/java/org/sopt/makers/operation/service/LectureServiceImpl.java +++ b/src/main/java/org/sopt/makers/operation/service/LectureServiceImpl.java @@ -1,12 +1,13 @@ package org.sopt.makers.operation.service; -import static java.util.Objects.nonNull; import static org.sopt.makers.operation.common.ExceptionMessage.*; +import static org.sopt.makers.operation.common.ExceptionMessage.NO_SESSION; +import static org.sopt.makers.operation.dto.lecture.LectureResponseType.*; import static org.sopt.makers.operation.entity.AttendanceStatus.*; -import static org.sopt.makers.operation.entity.Part.*; import static org.sopt.makers.operation.entity.alarm.Attribute.*; import static org.sopt.makers.operation.entity.lecture.LectureStatus.*; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.*; @@ -14,6 +15,7 @@ import lombok.val; +import org.sopt.makers.operation.config.ValueConfig; import org.sopt.makers.operation.dto.alarm.AlarmSenderDTO; import org.sopt.makers.operation.dto.lecture.*; @@ -22,18 +24,17 @@ import org.sopt.makers.operation.dto.lecture.LectureRequestDTO; import org.sopt.makers.operation.dto.lecture.LectureResponseDTO; import org.sopt.makers.operation.dto.lecture.LecturesResponseDTO; -import org.sopt.makers.operation.dto.member.MemberSearchCondition; import org.sopt.makers.operation.entity.*; import org.sopt.makers.operation.entity.lecture.Attribute; import org.sopt.makers.operation.entity.lecture.Lecture; import org.sopt.makers.operation.exception.LectureException; +import org.sopt.makers.operation.exception.SubLectureException; import org.sopt.makers.operation.external.api.AlarmSender; import org.sopt.makers.operation.repository.attendance.AttendanceRepository; import org.sopt.makers.operation.repository.SubAttendanceRepository; import org.sopt.makers.operation.repository.lecture.LectureRepository; import org.sopt.makers.operation.repository.lecture.SubLectureRepository; import org.sopt.makers.operation.repository.member.MemberRepository; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,113 +44,69 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class LectureServiceImpl implements LectureService { + private final LectureRepository lectureRepository; private final SubLectureRepository subLectureRepository; private final AttendanceRepository attendanceRepository; private final SubAttendanceRepository subAttendanceRepository; private final MemberRepository memberRepository; private final AlarmSender alarmSender; + private final ValueConfig valueConfig; - @Value("${sopt.alarm.message.title_end}") - private String ALARM_MESSAGE_TITLE; - @Value("${sopt.alarm.message.content_end}") - private String ALARM_MESSAGE_CONTENT; + /** WEB **/ @Override @Transactional - public Long createLecture(LectureRequestDTO requestDTO) { - // 세션 생성 - Lecture savedLecture = lectureRepository.save(requestDTO.toEntity()); - - // 출석 세션 2개 생성 - Stream.iterate(1, i -> i + 1).limit(2) - .forEach(i -> subLectureRepository.save(new SubLecture(savedLecture, i))); - - // 출석 생성 - memberRepository - .search(getMemberSearchCondition(requestDTO)) - .forEach(member -> attendanceRepository.save(new Attendance(member, savedLecture))); - - // 서브 출석 생성 - savedLecture.getAttendances() - .forEach(attendance -> savedLecture.getSubLectures() - .forEach(subLecture -> subAttendanceRepository.save(new SubAttendance(attendance, subLecture)))); - - + public long createLecture(LectureRequestDTO request) { + val savedLecture = saveLecture(request); + createSubLectures(savedLecture); + createAttendance(request.generation(), request.part(), savedLecture); + createSubAttendances(savedLecture); return savedLecture.getId(); } - @Override - public LectureGetResponseDTO getCurrentLecture(Long playGroundId) { - val now = LocalDateTime.now(); - - val attendances = attendanceRepository.findCurrentAttendanceByMember(playGroundId); - - if (attendances.isEmpty()) { - return new LectureGetResponseDTO(LectureResponseType.NO_SESSION, 0L, "", "", "", "", "", Collections.emptyList()); - } - - if (attendances.size() > 2) { - throw new LectureException(INVALID_COUNT_SESSION.getName()); - } - - // 현재 출석과 Lecture 가져오기 - val currentAttendance = getCurrentAttendance(attendances, now); - val currentLecture = currentAttendance.getLecture(); - val lectureType = getLectureResponseType(currentLecture); - - if (lectureType.equals(LectureResponseType.NO_ATTENDANCE)) { - val message = "출석 점수가 반영되지 않아요."; - return LectureGetResponseDTO.of(lectureType, currentLecture, message, Collections.emptyList()); - } - - val subAttendances = attendanceRepository.findSubAttendanceByAttendanceId(currentAttendance.getId()); - - val firstSubLectureAttendance = subAttendances.get(0); - val secondSubLectureAttendance = subAttendances.get(1); - - val firstSubLectureAttendanceStatus = firstSubLectureAttendance.getStatus(); - val secondSubLectureAttendanceStatus = secondSubLectureAttendance.getStatus(); + private Lecture saveLecture(LectureRequestDTO request) { + val lecture = request.toEntity(); + return lectureRepository.save(lecture); + } - val firstSessionStart = firstSubLectureAttendance.getSubLecture().getStartAt(); - val secondSessionStart = secondSubLectureAttendance.getSubLecture().getStartAt(); + private void createSubLectures(Lecture lecture) { + Stream.iterate(1, i -> i + 1).limit(valueConfig.getSUB_LECTURE_MAX_ROUND()) + .forEach(round -> saveSubLecture(lecture, round)); + } - val message = (currentLecture.getAttribute() == Attribute.SEMINAR) ? "" : "행사도 참여하고, 출석점수도 받고, 일석이조!"; + private void saveSubLecture(Lecture lecture, int round) { + subLectureRepository.save(new SubLecture(lecture, round)); + } - // Lecture 시작 전 혹은 1차 출석 시작 전 - if (now.isBefore(currentLecture.getStartDate()) || !nonNull(firstSessionStart)) { - return LectureGetResponseDTO.of(lectureType, currentLecture, message, Collections.emptyList()); - } + private void createAttendance(int generation, Part part, Lecture lecture) { + memberRepository.find(generation, part).forEach(member -> saveAttendance(member, lecture)); + } - // 1차 출석 시작, 2차 출석 시작 전 - if (now.isAfter(firstSessionStart) && !nonNull(secondSessionStart)) { - // 1차 출석 중 결석인 상태 - if (now.isBefore(firstSessionStart.plusMinutes(10)) && firstSubLectureAttendanceStatus.equals(ABSENT)) { - return LectureGetResponseDTO.of(lectureType, currentLecture, message, Collections.emptyList()); - } + private void saveAttendance(Member member, Lecture lecture) { + attendanceRepository.save(new Attendance(member, lecture)); + } - return LectureGetResponseDTO.of(lectureType, currentLecture, message, Collections.singletonList(firstSubLectureAttendance)); - } + private void createSubAttendances(Lecture lecture) { + lecture.getAttendances().forEach(this::saveSubAttendances); + } - // 2차 출석 시작 이후 - if (now.isAfter(secondSessionStart)) { - // 2차 출석 중 결석인 상태 - if (now.isBefore(secondSessionStart.plusMinutes(10)) && secondSubLectureAttendanceStatus.equals(ABSENT)) { - return LectureGetResponseDTO.of(lectureType, currentLecture, message, Collections.singletonList(firstSubLectureAttendance)); - } - } - return LectureGetResponseDTO.of(lectureType, currentLecture, message, subAttendances); + private void saveSubAttendances(Attendance attendance) { + attendance.getLecture().getSubLectures().forEach(subLecture -> saveSubAttendance(attendance, subLecture)); } + private void saveSubAttendance(Attendance attendance, SubLecture subLecture) { + subAttendanceRepository.save(new SubAttendance(attendance, subLecture)); + } @Override - public LecturesResponseDTO getLecturesByGeneration(int generation, Part part) { - val lectures = lectureRepository.findLectures(generation, part); + public LecturesResponseDTO getLectures(int generation, Part part) { + val lectures = lectureRepository.find(generation, part); return LecturesResponseDTO.of(generation, lectures); } @Override - public LectureResponseDTO getLecture(Long lectureId) { + public LectureResponseDTO getLecture(long lectureId) { Lecture lecture = findLecture(lectureId); return LectureResponseDTO.of(lecture); } @@ -157,159 +114,224 @@ public LectureResponseDTO getLecture(Long lectureId) { @Override @Transactional public AttendanceResponseDTO startAttendance(AttendanceRequestDTO requestDTO) { - Lecture lecture = findLecture(requestDTO.lectureId()); + val lecture = findLecture(requestDTO.lectureId()); + checkStartAttendanceValidity(lecture, requestDTO.round()); + val subLecture = getSubLecture(lecture, requestDTO.round()); + subLecture.startAttendance(requestDTO.code()); + return AttendanceResponseDTO.of(lecture, subLecture); + } - // 출석 가능 여부 유효성 체크 - if (requestDTO.round() == 2 && lecture.getLectureStatus().equals(BEFORE)) { - throw new IllegalStateException(NOT_STARTED_PRE_ATTENDANCE.getName()); - } else if (lecture.getLectureStatus().equals(END)) { - throw new IllegalStateException(END_LECTURE.getName()); + private void checkStartAttendanceValidity(Lecture lecture, int round) { + if (lecture.isEnd()) { + throw new LectureException(END_LECTURE.getName()); + } else if (round == 2 && lecture.isBefore()) { + throw new LectureException(NOT_STARTED_PRE_ATTENDANCE.getName()); } + } - // 출석 세션 상태 업데이트 (시작) - SubLecture subLecture = lecture.getSubLectures().stream() - .filter(session -> session.getRound() == requestDTO.round()) - .findFirst() - .orElseThrow(() -> new IllegalStateException(NO_SUB_LECTURE_EQUAL_ROUND.getName())); - subLecture.startAttendance(requestDTO.code()); - - return new AttendanceResponseDTO(lecture.getId(), subLecture.getId()); + @Override + @Transactional + public void endLecture(Long lectureId) { + val lecture = findLecture(lectureId); + checkEndLectureValidity(lecture); + lecture.updateToEnd(); + sendAlarm(lecture); } @Override @Transactional - public void finishLecture(Long lectureId) { + public void deleteLecture(Long lectureId) { val lecture = findLecture(lectureId); - val now = LocalDateTime.now(); - if (now.isBefore(lecture.getEndDate())) { - throw new IllegalStateException(NOT_END_TIME_YET.getName()); + if (lecture.isEnd()) { + restoreAttendances(lecture.getAttendances()); } - lecture.finish(); + subAttendanceRepository.deleteAllBySubLectureIn(lecture.getSubLectures()); + subLectureRepository.deleteAllByLecture(lecture); + attendanceRepository.deleteAllByLecture(lecture); + lectureRepository.deleteById(lectureId); + } - List memberPgIds = lecture.getAttendances().stream() - .map(attendance -> String.valueOf(attendance.getMember().getPlaygroundId())) - .filter(id -> !id.equals("null")) - .toList(); + private void restoreAttendances(List attendances) { + attendances.forEach(Attendance::revertMemberScore); + } - val alarmTitle = lecture.getName() + " " + ALARM_MESSAGE_TITLE; - alarmSender.send(new AlarmSenderDTO(alarmTitle, ALARM_MESSAGE_CONTENT, memberPgIds, NEWS, null)); + @Override + public LectureDetailResponseDTO getLectureDetail(long lectureId) { + val lecture = findLecture(lectureId); + return LectureDetailResponseDTO.of(lecture); } + private Lecture findLecture(Long id) { + return lectureRepository.findById(id) + .orElseThrow(() -> new LectureException(INVALID_LECTURE.getName())); + } + + /** SCHEDULER **/ + @Override @Transactional - public void finishLecture() { + public void endLectures() { val lectures = lectureRepository.findLecturesToBeEnd(); - lectures.forEach(Lecture::finish); - - List memberPgIds; - for (val lecture : lectures) { - memberPgIds = new ArrayList<>(); - for (val attendance : lecture.getAttendances()) { - val playgroundId = attendance.getMember().getPlaygroundId(); - if (Objects.nonNull(playgroundId)) { - memberPgIds.add(String.valueOf(attendance.getMember().getPlaygroundId())); - } - } - - val alarmTitle = lecture.getName() + " " + ALARM_MESSAGE_TITLE; - alarmSender.send(new AlarmSenderDTO(alarmTitle, ALARM_MESSAGE_CONTENT, memberPgIds, NEWS, null)); + lectures.forEach(lecture -> endLecture(lecture.getId())); + } + + private void checkEndLectureValidity(Lecture lecture) { + if (!lecture.isEnd()) { + throw new LectureException(NOT_END_TIME_YET.getName()); } } - @Override - public LectureCurrentRoundResponseDTO getCurrentLectureRound(Long lectureId) { - val now = LocalDateTime.now(); - val today = now.toLocalDate(); - val startOfDay = today.atStartOfDay(); - val endOfDay = LocalDateTime.of(today, LocalTime.MAX); + private void sendAlarm(Lecture lecture) { + val alarmTitle = getAlarmTitle(lecture); + val alarmContent = valueConfig.getALARM_MESSAGE_CONTENT(); + val memberPlaygroundIds = getMemberPlaygroundIds(lecture); + alarmSender.send(new AlarmSenderDTO(alarmTitle, alarmContent, memberPlaygroundIds, NEWS, null)); + } - val lecture = lectureRepository.findById(lectureId) - .orElseThrow(() -> new LectureException(INVALID_LECTURE.getName())); + private List getMemberPlaygroundIds(Lecture lecture) { + return lecture.getAttendances().stream() + .map(attendance -> String.valueOf(attendance.getMember().getPlaygroundId())) + .filter(id -> !id.equals("null")) + .toList(); + } - val lectureStartDate = lecture.getStartDate(); - val lectureStatus = lecture.getLectureStatus(); + private String getAlarmTitle(Lecture lecture) { + return lecture.getName() + " " + valueConfig.getALARM_MESSAGE_TITLE(); + } - val subLectures = lecture.getSubLectures(); - subLectures.sort(Comparator.comparing(SubLecture::getRound)); + /** APP **/ - val subLecture = lectureStatus.equals(FIRST) ? - subLectures.get(0) : subLectures.get(1); + @Override + public TodayLectureResponseDTO getTodayLecture(long memberPlaygroundId) { + val attendances = attendanceRepository.findToday(memberPlaygroundId); + checkAttendancesSize(attendances); - if (lectureStartDate.isBefore(startOfDay) || lectureStartDate.isAfter(endOfDay)) { - throw new LectureException(NO_SESSION.getName()); + if (attendances.isEmpty()) { + return getEmptyResponse(); } - if (lectureStatus.equals(BEFORE)) { - throw new LectureException(NOT_STARTED_ATTENDANCE.getName()); - } + val attendance = getNowAttendance(attendances); + val lecture = attendance.getLecture(); + val responseType = getResponseType(lecture); + val message = getMessage(lecture.getAttribute()); - if (lectureStatus.equals(FIRST)) { - // 1차 출석이 마감되었을 때 - if (now.isAfter(subLecture.getStartAt().plusMinutes(10))) { - throw new LectureException(subLecture.getRound() + ENDED_ATTENDANCE.getName()); - } + if (responseType.equals(NO_ATTENDANCE) || lecture.isBefore()) { + return TodayLectureResponseDTO.of(responseType, lecture, message, Collections.emptyList()); } - if (lectureStatus.equals(SECOND)) { - // 2차 출석이 마감되었을 때 - if (now.isAfter(subLecture.getStartAt().plusMinutes(10))) { - throw new LectureException(subLecture.getRound() + ENDED_ATTENDANCE.getName()); - } - } + val subAttendances = attendance.getSubAttendances(); + val subAttendance = lecture.isFirst() ? subAttendances.get(0) : subAttendances.get(1); + return getTodayLectureResponse(subAttendance, responseType, lecture); + } - if (lectureStatus.equals(END)) { - throw new LectureException(END_LECTURE.getName()); + private TodayLectureResponseDTO getEmptyResponse() { + return TodayLectureResponseDTO.builder() + .type(LectureResponseType.NO_SESSION) + .id(0L) + .location("") + .name("") + .startDate("") + .endDate("") + .message("") + .attendances(Collections.emptyList()) + .build(); + } + + private void checkAttendancesSize(List attendances) { + if (attendances.size() > valueConfig.getSUB_LECTURE_MAX_ROUND()) { + throw new LectureException(INVALID_COUNT_SESSION.getName()); } + } - return LectureCurrentRoundResponseDTO.of(subLecture); + private Attendance getNowAttendance(List attendances) { + val index = getAttendanceIndex(); + return attendances.get(index); } - @Override - @Transactional - public void deleteLecture(Long lectureId) { - val lecture = lectureRepository.find(lectureId) - .orElseThrow(() -> new LectureException(INVALID_LECTURE.getName())); + private int getAttendanceIndex() { + return (LocalDateTime.now().getHour() >= 16) ? 1 : 0; + } - // 출석 종료된 세션: 출석 점수 갱신 전으로 복구 - if (lecture.getLectureStatus().equals(END)) { - lecture.getAttendances().forEach(Attendance::revertMemberScore); - } + private LectureResponseType getResponseType(Lecture lecture) { + val attribute = lecture.getAttribute(); + return attribute.equals(Attribute.ETC) ? NO_ATTENDANCE : HAS_ATTENDANCE; + } - // 연관 관계의 객체 삭제 후 세션 삭제 - subAttendanceRepository.deleteAllBySubLectureIn(lecture.getSubLectures()); - subLectureRepository.deleteAllByLecture(lecture); - attendanceRepository.deleteAllByLecture(lecture); - lectureRepository.deleteById(lectureId); - // lectureRepository.delete(lecture); //TODO: 에러 원인 파악 필요 + private String getMessage(Attribute attribute) { + return switch (attribute) { + case SEMINAR -> valueConfig.getSEMINAR_MESSAGE(); + case EVENT -> valueConfig.getEVENT_MESSAGE(); + case ETC -> valueConfig.getETC_MESSAGE(); + }; + } + + private TodayLectureResponseDTO getTodayLectureResponse(SubAttendance subAttendance, LectureResponseType responseType, Lecture lecture) { + val subLecture = subAttendance.getSubLecture(); + val isOnAttendanceCheck = LocalDateTime.now().isBefore(subLecture.getStartAt().plusMinutes(10)); + val message = getMessage(lecture.getAttribute()); + if (isOnAttendanceCheck && subAttendance.getStatus().equals(ABSENT)) { + return TodayLectureResponseDTO.of(responseType, lecture, message, Collections.emptyList()); + } + return TodayLectureResponseDTO.of(responseType, lecture, message, Collections.singletonList(subAttendance)); } @Override - public LectureDetailResponseDTO getLectureDetail(Long lectureId) { + public LectureCurrentRoundResponseDTO getCurrentLectureRound(long lectureId) { val lecture = findLecture(lectureId); - return LectureDetailResponseDTO.of(lecture); + val subLecture = getSubLecture(lecture); + checkLectureExist(lecture); + checkLectureBefore(lecture); + checkEndAttendance(subLecture); + checkLectureEnd(lecture); + return LectureCurrentRoundResponseDTO.of(subLecture); } - private MemberSearchCondition getMemberSearchCondition(LectureRequestDTO requestDTO) { - return new MemberSearchCondition( - !requestDTO.part().equals(ALL) ? requestDTO.part() : null, - requestDTO.generation() - ); + private SubLecture getSubLecture(Lecture lecture) { + val status = lecture.getLectureStatus(); + val round = status.equals(FIRST) ? 1 : 2; + return getSubLecture(lecture, round); } - private Lecture findLecture(Long id) { - return lectureRepository.findById(id) - .orElseThrow(() -> new LectureException(INVALID_LECTURE.getName())); + private SubLecture getSubLecture(Lecture lecture, int round) { + return lecture.getSubLectures().stream() + .filter(l -> l.getRound() == round) + .findFirst() + .orElseThrow(() -> new SubLectureException(NO_SUB_LECTURE_EQUAL_ROUND.getName())); } - private Attendance getCurrentAttendance(List attendances, LocalDateTime now) { - val lectureSize = attendances.size(); - val currentHour = now.getHour(); + private void checkLectureExist(Lecture lecture) { + val today = LocalDate.now(); + val startOfDay = today.atStartOfDay(); + val endOfDay = LocalDateTime.of(today, LocalTime.MAX); + val startAt = lecture.getStartDate(); + if (startAt.isBefore(startOfDay) || startAt.isAfter(endOfDay)) { + throw new LectureException(NO_SESSION.getName()); + } + } - val attendanceIndex = (lectureSize == 2 && currentHour >= 16) ? 1 : 0; - return attendances.get(attendanceIndex); + private void checkLectureBefore(Lecture lecture) { + if (lecture.isBefore()) { + throw new LectureException(NOT_STARTED_ATTENDANCE.getName()); + } } - private LectureResponseType getLectureResponseType(Lecture currentLecture) { - return (currentLecture.getAttribute() != Attribute.ETC) ? LectureResponseType.HAS_ATTENDANCE : LectureResponseType.NO_ATTENDANCE; + private void checkEndAttendance(SubLecture subLecture) { + if (isEndAttendance(subLecture)) { + throw new LectureException(subLecture.getRound() + ENDED_ATTENDANCE.getName()); + } + } + + private boolean isEndAttendance(SubLecture subLecture) { + val status = subLecture.getLecture().getLectureStatus(); + if (LocalDateTime.now().isAfter(subLecture.getStartAt().plusMinutes(10))) { + return status.equals(FIRST) || status.equals(SECOND); + } + return false; + } + + private void checkLectureEnd(Lecture lecture) { + if (lecture.isEnd()) { + throw new LectureException(END_LECTURE.getName()); + } } } diff --git a/src/main/java/org/sopt/makers/operation/service/MemberServiceImpl.java b/src/main/java/org/sopt/makers/operation/service/MemberServiceImpl.java index d4885baf..e1f57251 100644 --- a/src/main/java/org/sopt/makers/operation/service/MemberServiceImpl.java +++ b/src/main/java/org/sopt/makers/operation/service/MemberServiceImpl.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.val; +import org.sopt.makers.operation.config.ValueConfig; import org.sopt.makers.operation.dto.attendance.AttendanceTotalCountVO; import org.sopt.makers.operation.dto.attendance.AttendanceTotalResponseDTO; import org.sopt.makers.operation.dto.attendance.AttendanceTotalVO; @@ -20,7 +21,6 @@ import org.sopt.makers.operation.exception.MemberException; import org.sopt.makers.operation.repository.attendance.AttendanceRepository; import org.sopt.makers.operation.repository.member.MemberRepository; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -34,9 +34,7 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; private final AttendanceRepository attendanceRepository; - - @Value("${sopt.current.generation}") - private int currentGeneration; + private final ValueConfig valueConfig; @Override public MembersResponseDTO getMemberList(Part part, int generation, Pageable pageable) { @@ -58,7 +56,7 @@ public MembersResponseDTO getMemberList(Part part, int generation, Pageable page @Override public AttendanceTotalResponseDTO getMemberTotalAttendance(Long playGroundId) { - val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, currentGeneration) + val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, valueConfig.getGENERATION()) .orElseThrow(() -> new MemberException(INVALID_MEMBER.getName())); val attendances = findAttendances(member); @@ -72,7 +70,7 @@ public AttendanceTotalResponseDTO getMemberTotalAttendance(Long playGroundId) { @Override public MemberScoreGetResponse getMemberScore(Long playGroundId) { - val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, currentGeneration) + val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, valueConfig.getGENERATION()) .orElseThrow(() -> new MemberException(INVALID_MEMBER.getName())); return MemberScoreGetResponse.of(member.getScore()); diff --git a/src/test/java/org/sopt/makers/operation/controller/web/LectureControllerTest.java b/src/test/java/org/sopt/makers/operation/controller/web/LectureControllerTest.java new file mode 100644 index 00000000..f00c2aa4 --- /dev/null +++ b/src/test/java/org/sopt/makers/operation/controller/web/LectureControllerTest.java @@ -0,0 +1,159 @@ +package org.sopt.makers.operation.controller.web; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.sopt.makers.operation.dto.lecture.AttendanceRequestDTO; +import org.sopt.makers.operation.dto.lecture.AttendanceResponseDTO; +import org.sopt.makers.operation.dto.lecture.LectureDetailResponseDTO; +import org.sopt.makers.operation.dto.lecture.LectureRequestDTO; +import org.sopt.makers.operation.dto.lecture.LectureResponseDTO; +import org.sopt.makers.operation.dto.lecture.LecturesResponseDTO; +import org.sopt.makers.operation.service.LectureService; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.mockito.Mockito.*; +import static org.sopt.makers.operation.fixture.LectureFixture.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.google.gson.Gson; + +@ExtendWith(MockitoExtension.class) +class LectureControllerTest { + + @InjectMocks + private LectureController lectureController; + + @Mock + private LectureService lectureService; + + private MockMvc mockMvc; + + private final String DEFAULT_URI = "/api/v1/lectures"; + + @BeforeEach + public void init() { + mockMvc = MockMvcBuilders.standaloneSetup(lectureController).build(); + } + + @DisplayName("세션 생성 성공") + @Test + void success_createLecture() throws Exception { + // given + LectureRequestDTO request = lectureRequest(); + long response = lectureId(); + + doReturn(response).when(lectureService).createLecture(any()); + + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.post(DEFAULT_URI) + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(request))); + + // then + resultActions.andExpect(status().isCreated()); + } + + @DisplayName("세션 목록 조회 성공") + @Test + void success_getLectureList() throws Exception { + // given + LecturesResponseDTO response = lecturesResponse(); + MultiValueMap queries = new LinkedMultiValueMap<>(); + queries.add("generation", String.valueOf(LECTURE_GENERATION)); + + doReturn(response).when(lectureService).getLectures(anyInt(), any()); + + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.get(DEFAULT_URI) + .queryParams(queries)); + + // then + resultActions.andExpect(status().isOk()); + } + + @DisplayName("세션 단일 조회 성공") + @Test + void success_getLecture() throws Exception { + // given + LectureResponseDTO response = lectureResponse(); + + doReturn(response).when(lectureService).getLecture(anyLong()); + + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.get(DEFAULT_URI + "/{lectureId}", anyLong())); + + // then + resultActions.andExpect(status().isOk()); + } + + @DisplayName("출석 시작 성공") + @Test + void success_startAttendance() throws Exception { + // given + AttendanceRequestDTO request = attendanceRequest(); + AttendanceResponseDTO response = attendanceResponse(); + + doReturn(response).when(lectureService).startAttendance(any()); + + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.patch(DEFAULT_URI + "/attendance") + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(request))); + + // then + resultActions.andExpect(status().isCreated()); + } + + @DisplayName("출석 종료 성공") + @Test + void success_finishLecture() throws Exception { + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.patch(DEFAULT_URI + "/{lectureId}", anyLong())); + + // then + resultActions.andExpect(status().isOk()); + } + + @DisplayName("세션 삭제 성공") + @Test + void success_deleteLecture() throws Exception { + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.delete(DEFAULT_URI + "/{lectureId}", anyLong())); + + // then + resultActions.andExpect(status().isOk()); + } + + @DisplayName("세션 상세 조회 성공") + @Test + void success_getLectureDetail() throws Exception { + // given + LectureDetailResponseDTO response = lectureDetail(); + + doReturn(response).when(lectureService).getLectureDetail(anyLong()); + + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders.get(DEFAULT_URI + "/detail/{lectureId}", anyLong())); + + // then + resultActions.andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/org/sopt/makers/operation/fixture/LectureFixture.java b/src/test/java/org/sopt/makers/operation/fixture/LectureFixture.java new file mode 100644 index 00000000..cad87a67 --- /dev/null +++ b/src/test/java/org/sopt/makers/operation/fixture/LectureFixture.java @@ -0,0 +1,145 @@ +package org.sopt.makers.operation.fixture; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Stream; + +import org.sopt.makers.operation.dto.lecture.AttendanceRequestDTO; +import org.sopt.makers.operation.dto.lecture.AttendanceResponseDTO; +import org.sopt.makers.operation.dto.lecture.AttendancesStatusVO; +import org.sopt.makers.operation.dto.lecture.LectureDetailResponseDTO; +import org.sopt.makers.operation.dto.lecture.LectureRequestDTO; +import org.sopt.makers.operation.dto.lecture.LectureResponseDTO; +import org.sopt.makers.operation.dto.lecture.LectureResponseDTO.SubLectureVO; +import org.sopt.makers.operation.dto.lecture.LecturesResponseDTO; +import org.sopt.makers.operation.dto.lecture.LecturesResponseDTO.LectureVO; +import org.sopt.makers.operation.entity.Part; +import org.sopt.makers.operation.entity.SubLecture; +import org.sopt.makers.operation.entity.lecture.Attribute; +import org.sopt.makers.operation.entity.lecture.Lecture; +import org.sopt.makers.operation.entity.lecture.LectureStatus; + +public class LectureFixture { + + public static final String LECTURE_NAME = "테스트 이름"; + public static final Part LECTURE_PART = Part.ALL; + public static final int LECTURE_GENERATION = 30; + public static final String LECTURE_PLACE = "테스트 장소"; + public static final LocalDateTime NOW = LocalDateTime.now(); + public static final Attribute LECTURE_ATTRIBUTE = Attribute.ETC; + public static final LectureStatus LECTURE_STATUS = LectureStatus.BEFORE; + public static final int LIST_SIZE = 5; + public static final String SUB_LECTURE_CODE = "code"; + + public static LectureRequestDTO lectureRequest() { + return LectureRequestDTO.builder() + .part(LECTURE_PART) + .name(LECTURE_NAME) + .generation(LECTURE_GENERATION) + .place(LECTURE_PLACE) + .startDate(NOW.toString()) + .endDate(NOW.plusHours(4).toString()) + .attribute(LECTURE_ATTRIBUTE) + .build(); + } + + public static Lecture lectureEnd() { + Lecture lecture = lectureRequest().toEntity(); + lecture.updateToEnd(); + return lecture; + } + + public static long lectureId() { + return 0L; + } + + public static LecturesResponseDTO lecturesResponse() { + return new LecturesResponseDTO(LECTURE_GENERATION, lectures()); + } + + private static List lectures() { + return Stream.iterate(1, i -> i + 1).limit(LIST_SIZE) + .map(LectureFixture::lecture).toList(); + } + + private static LectureVO lecture(int i) { + return LectureVO.builder() + .lectureId(0L) + .name(LECTURE_NAME + i) + .partValue(Part.ALL) + .partName(Part.ALL.getName()) + .startDate(NOW.plusHours(i).toString()) + .endDate(NOW.plusHours(i + 4).toString()) + .attributeValue(LECTURE_ATTRIBUTE) + .attributeName(LECTURE_ATTRIBUTE.getName()) + .place(LECTURE_PLACE + i) + .attendances(attendancesStatus()) + .build(); + } + + public static LectureResponseDTO lectureResponse() { + return LectureResponseDTO.builder() + .lectureId(0L) + .name(LECTURE_NAME) + .generation(LECTURE_GENERATION) + .part(LECTURE_PART) + .attribute(LECTURE_ATTRIBUTE) + .subLectures(subLectures()) + .attendances(attendancesStatus()) + .status(LECTURE_STATUS) + .build(); + } + + private static AttendancesStatusVO attendancesStatus() { + return AttendancesStatusVO.builder() + .attendance(80) + .absent(0) + .tardy(10) + .unknown(10) + .build(); + } + + private static List subLectures() { + return Stream.iterate(1, i -> i + 1).limit(LIST_SIZE) + .map(LectureFixture::subLecture).toList(); + } + + private static SubLectureVO subLecture(int i) { + return SubLectureVO.builder() + .subLectureId((long)i) + .round(i % 2) + .startAt(NOW.toString()) + .code(SUB_LECTURE_CODE) + .build(); + } + + public static AttendanceResponseDTO attendanceResponse() { + return new AttendanceResponseDTO(0L, 0L); + } + + public static AttendanceRequestDTO attendanceRequest() { + return new AttendanceRequestDTO(0L, 1, SUB_LECTURE_CODE); + } + + public static LectureDetailResponseDTO lectureDetail() { + return LectureDetailResponseDTO.builder() + .lectureId(0L) + .part(LECTURE_PART.getName()) + .name(LECTURE_NAME) + .place(LECTURE_PLACE) + .attribute(LECTURE_ATTRIBUTE.getName()) + .startDate(NOW.toString()) + .endDate(NOW.plusHours(4).toString()) + .generation(LECTURE_GENERATION) + .build(); + } + + public static List lectureList() { + return Stream.iterate(1, i -> i + 1).limit(LIST_SIZE) + .map(i -> lectureRequest().toEntity()).toList(); + } + + public static SubLecture subLecture(Lecture lecture) { + return new SubLecture(lecture, 1); + } +} diff --git a/src/test/java/org/sopt/makers/operation/service/LectureServiceImplTest.java b/src/test/java/org/sopt/makers/operation/service/LectureServiceImplTest.java new file mode 100644 index 00000000..e37a615f --- /dev/null +++ b/src/test/java/org/sopt/makers/operation/service/LectureServiceImplTest.java @@ -0,0 +1,111 @@ +package org.sopt.makers.operation.service; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.sopt.makers.operation.fixture.LectureFixture.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.sopt.makers.operation.config.ValueConfig; +import org.sopt.makers.operation.dto.lecture.AttendanceRequestDTO; +import org.sopt.makers.operation.dto.lecture.LectureResponseDTO; +import org.sopt.makers.operation.dto.lecture.LecturesResponseDTO; +import org.sopt.makers.operation.entity.lecture.Lecture; +import org.sopt.makers.operation.external.api.AlarmSender; +import org.sopt.makers.operation.repository.lecture.LectureRepository; +import org.sopt.makers.operation.repository.member.MemberRepository; + +import static org.mockito.Mockito.*; + +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class LectureServiceImplTest { + + @InjectMocks + LectureServiceImpl lectureService; + + @Mock + private LectureRepository lectureRepository; + @Mock + private MemberRepository memberRepository; + @Mock + private AlarmSender alarmSender; + @Mock + ValueConfig valueConfig; + + @DisplayName("세션 목록 조회") + @Test + void getLectures() { + // given + doReturn(lectureList()).when(lectureRepository).find(anyInt(), any()); + + // when + LecturesResponseDTO response = lectureService.getLectures(LECTURE_GENERATION, LECTURE_PART); + + // then + assertThat(response.lectures().size(), is(equalTo(LIST_SIZE))); + + // verify + verify(lectureRepository, times(1)).find(anyInt(), any()); + } + + @DisplayName("세션 단건 조회") + @Test + void getLecture() { + // given + Lecture lecture = lectureRequest().toEntity(); + long lectureId = lectureId(); + + doReturn(Optional.of(lecture)).when(lectureRepository).findById(anyLong()); + + // when + LectureResponseDTO response = lectureService.getLecture(lectureId); + + // then + assertThat(response.name(), is(equalTo(lecture.getName()))); + assertThat(response.generation(), is(equalTo(lecture.getGeneration()))); + assertThat(response.part(), is(equalTo(lecture.getPart()))); + assertThat(response.attribute(), is(equalTo(lecture.getAttribute()))); + + // verify + verify(lectureRepository, times(1)).findById(anyLong()); + } + + @DisplayName("출석 시작") + @Test + void startAttendance() { + // given + Lecture lecture = lectureRequest().toEntity(); + subLecture(lecture); + AttendanceRequestDTO request = attendanceRequest(); + + doReturn(Optional.of(lecture)).when(lectureRepository).findById(anyLong()); + + // when + lectureService.startAttendance(request); + + // verify + verify(lectureRepository, times(1)).findById(anyLong()); + } + + @DisplayName("세션 종료") + @Test + void endLecture() { + // given + long lectureId = lectureId(); + Lecture lecture = lectureEnd(); + + doReturn(Optional.of(lecture)).when(lectureRepository).findById(anyLong()); + + // when + lectureService.endLecture(lectureId); + + // verify + verify(lectureRepository, times(1)).findById(anyLong()); + } +} \ No newline at end of file