From d51410984323894ecf5894275fe3f8839f5c726e Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Thu, 9 Oct 2025 13:22:23 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=20=EB=B0=94?= =?UTF-8?q?=EB=94=94=20json=20=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=A7=80=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attendances/application/AttendanceService.java | 5 +++-- .../controller/api/AttendanceController.java | 5 +++-- .../presentation/req/AttendanceUpdateReq.java | 12 ++++++++++++ .../application/AttendanceServiceTest.java | 9 +++++---- 4 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java diff --git a/src/main/java/com/mos/backend/attendances/application/AttendanceService.java b/src/main/java/com/mos/backend/attendances/application/AttendanceService.java index 4b70464d..95c872e8 100644 --- a/src/main/java/com/mos/backend/attendances/application/AttendanceService.java +++ b/src/main/java/com/mos/backend/attendances/application/AttendanceService.java @@ -6,6 +6,7 @@ import com.mos.backend.attendances.entity.AttendanceStatus; import com.mos.backend.attendances.entity.exception.AttendanceErrorCode; import com.mos.backend.attendances.infrastructure.AttendanceRepository; +import com.mos.backend.attendances.presentation.req.AttendanceUpdateReq; import com.mos.backend.common.exception.MosException; import com.mos.backend.common.infrastructure.EntityFacade; import com.mos.backend.studies.entity.Study; @@ -59,7 +60,7 @@ public void create(Long userId, Long studyId, Long studyScheduleId) { } @Transactional - public void update(Long userId, Long studyId, Long studyScheduleId, String attendanceStatusDescription) { + public void update(Long userId, Long studyId, Long studyScheduleId, AttendanceUpdateReq req) { User user = entityFacade.getUser(userId); Study study = entityFacade.getStudy(studyId); StudySchedule studySchedule = entityFacade.getStudySchedule(studyScheduleId); @@ -70,7 +71,7 @@ public void update(Long userId, Long studyId, Long studyScheduleId, String atten StudyMember studyMember = studyMemberRepository.findByUserIdAndStudyId(user.getId(), study.getId()) .orElseThrow(() -> new MosException(StudyMemberErrorCode.STUDY_MEMBER_NOT_FOUND)); - AttendanceStatus attendanceStatus = AttendanceStatus.fromDescription(attendanceStatusDescription); + AttendanceStatus attendanceStatus = AttendanceStatus.fromDescription(req.getAttendanceStatus()); if (!attendanceStatus.isModifiable()) throw new MosException(AttendanceErrorCode.UNMODIFIABLE_STATUS); diff --git a/src/main/java/com/mos/backend/attendances/presentation/controller/api/AttendanceController.java b/src/main/java/com/mos/backend/attendances/presentation/controller/api/AttendanceController.java index 16fec347..a6e72f07 100644 --- a/src/main/java/com/mos/backend/attendances/presentation/controller/api/AttendanceController.java +++ b/src/main/java/com/mos/backend/attendances/presentation/controller/api/AttendanceController.java @@ -3,6 +3,7 @@ import com.mos.backend.attendances.application.AttendanceService; import com.mos.backend.attendances.application.res.StudyMemberAttendanceRes; import com.mos.backend.attendances.application.res.StudyScheduleAttendanceRateRes; +import com.mos.backend.attendances.presentation.req.AttendanceUpdateReq; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -28,8 +29,8 @@ public void create(@AuthenticationPrincipal Long userId, public void update(@AuthenticationPrincipal Long userId, @PathVariable Long studyId, @PathVariable Long studyScheduleId, - @RequestBody String attendanceStatus) { - attendanceService.update(userId, studyId, studyScheduleId, attendanceStatus); + @RequestBody AttendanceUpdateReq attendanceUpdateReq) { + attendanceService.update(userId, studyId, studyScheduleId, attendanceUpdateReq); } @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java b/src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java new file mode 100644 index 00000000..288fbf77 --- /dev/null +++ b/src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java @@ -0,0 +1,12 @@ +package com.mos.backend.attendances.presentation.req; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class AttendanceUpdateReq { + private String attendanceStatus; +} diff --git a/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java b/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java index 026e7144..9f84a6ba 100644 --- a/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java +++ b/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java @@ -4,6 +4,7 @@ import com.mos.backend.attendances.entity.AttendanceStatus; import com.mos.backend.attendances.entity.exception.AttendanceErrorCode; import com.mos.backend.attendances.infrastructure.AttendanceRepository; +import com.mos.backend.attendances.presentation.req.AttendanceUpdateReq; import com.mos.backend.common.exception.MosException; import com.mos.backend.common.infrastructure.EntityFacade; import com.mos.backend.studies.entity.Study; @@ -220,7 +221,7 @@ void updateAttendance_Success() { StudyMember studyMember = mock(StudyMember.class); Attendance attendance = mock(Attendance.class); Optional optionalAttendance = Optional.of(attendance); - AttendanceStatus modifiableAttendanceStatus = AttendanceStatus.EARLY_LEAVE; + AttendanceUpdateReq req = new AttendanceUpdateReq(AttendanceStatus.EARLY_LEAVE.getDescription()); when(entityFacade.getUser(userId)).thenReturn(user); when(entityFacade.getStudy(studyId)).thenReturn(study); @@ -233,7 +234,7 @@ void updateAttendance_Success() { when(attendanceRepository.findByStudyScheduleAndStudyMember(studySchedule, studyMember)).thenReturn(optionalAttendance); // When - attendanceService.update(userId, studyId, studyScheduleId, modifiableAttendanceStatus.getDescription()); + attendanceService.update(userId, studyId, studyScheduleId, req); // Then verify(entityFacade).getUser(userId); @@ -261,7 +262,7 @@ void updateAttendance_Fail() { StudyMember studyMember = mock(StudyMember.class); Attendance attendance = mock(Attendance.class); Optional optionalAttendance = Optional.of(attendance); - AttendanceStatus unModifiableAttendanceStatus = AttendanceStatus.EARLY_LEAVE; + AttendanceUpdateReq req = new AttendanceUpdateReq(AttendanceStatus.EARLY_LEAVE.getDescription()); when(entityFacade.getUser(userId)).thenReturn(user); when(entityFacade.getStudy(studyId)).thenReturn(study); @@ -274,7 +275,7 @@ void updateAttendance_Fail() { when(attendanceRepository.findByStudyScheduleAndStudyMember(studySchedule, studyMember)).thenReturn(optionalAttendance); // When - attendanceService.update(userId, studyId, studyScheduleId, unModifiableAttendanceStatus.getDescription()); + attendanceService.update(userId, studyId, studyScheduleId, req); // Then verify(entityFacade).getUser(userId); From 85b02b1202835505ffdcd2803aa8ffed03fbf812 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Thu, 9 Oct 2025 18:17:54 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20StudySettings=EB=A5=BC=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EB=B0=8F=20=EC=B6=9C=EC=84=9D=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/AttendanceService.java | 11 +-- .../attendances/entity/Attendance.java | 43 +++++--- .../common/infrastructure/EntityFacade.java | 9 ++ .../studyschedules/entity/StudySchedule.java | 24 ++--- .../studysettings/entity/StudySettings.java | 2 +- .../application/AttendanceServiceTest.java | 73 ++------------ .../attendances/entity/AttendanceTest.java | 98 +++++++++++++++++++ .../api/AttendanceControllerTest.java | 2 +- 8 files changed, 158 insertions(+), 104 deletions(-) create mode 100644 src/test/java/com/mos/backend/attendances/entity/AttendanceTest.java diff --git a/src/main/java/com/mos/backend/attendances/application/AttendanceService.java b/src/main/java/com/mos/backend/attendances/application/AttendanceService.java index 95c872e8..1941dc75 100644 --- a/src/main/java/com/mos/backend/attendances/application/AttendanceService.java +++ b/src/main/java/com/mos/backend/attendances/application/AttendanceService.java @@ -17,6 +17,7 @@ import com.mos.backend.studyschedules.entity.StudySchedule; import com.mos.backend.studyschedules.entity.exception.StudyScheduleErrorCode; import com.mos.backend.studyschedules.infrastructure.StudyScheduleRepository; +import com.mos.backend.studysettings.entity.StudySettings; import com.mos.backend.users.entity.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -39,6 +40,7 @@ public class AttendanceService { public void create(Long userId, Long studyId, Long studyScheduleId) { User user = entityFacade.getUser(userId); Study study = entityFacade.getStudy(studyId); + StudySettings studySettings = entityFacade.getStudySettings(studyId); StudySchedule studySchedule = entityFacade.getStudySchedule(studyScheduleId); validateRelation(study, studySchedule); @@ -49,12 +51,9 @@ public void create(Long userId, Long studyId, Long studyScheduleId) { validateAlreadyExist(studySchedule, studyMember); - if (studySchedule.isBeforePresentTime()) - throw new MosException(AttendanceErrorCode.NOT_PRESENT_TIME); - - Attendance attendance = studySchedule.isPresentTime() - ? Attendance.createPresentAttendance(studySchedule, studyMember) - : Attendance.createLateAttendance(studySchedule, studyMember); + Attendance attendance = Attendance.createWithThreshold( + studySchedule, studyMember, studySettings.getLateThresholdMinutes(), studySettings.getAbsenceThresholdMinutes() + ); attendanceRepository.save(attendance); } diff --git a/src/main/java/com/mos/backend/attendances/entity/Attendance.java b/src/main/java/com/mos/backend/attendances/entity/Attendance.java index a9fa9574..4845a884 100644 --- a/src/main/java/com/mos/backend/attendances/entity/Attendance.java +++ b/src/main/java/com/mos/backend/attendances/entity/Attendance.java @@ -1,14 +1,26 @@ package com.mos.backend.attendances.entity; +import com.mos.backend.attendances.entity.exception.AttendanceErrorCode; import com.mos.backend.common.entity.BaseTimeEntity; +import com.mos.backend.common.exception.MosException; import com.mos.backend.studymembers.entity.StudyMember; import com.mos.backend.studyschedules.entity.StudySchedule; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.OnDelete; +import java.time.LocalDateTime; + import static org.hibernate.annotations.OnDeleteAction.CASCADE; @Entity @@ -35,28 +47,27 @@ public class Attendance extends BaseTimeEntity { @Column(nullable = false) private AttendanceStatus attendanceStatus; - public static Attendance createPresentAttendance(StudySchedule studySchedule, StudyMember studyMember) { + public static Attendance create(StudySchedule studySchedule, StudyMember studyMember, AttendanceStatus attendanceStatus) { Attendance attendance = new Attendance(); attendance.studySchedule = studySchedule; attendance.studyMember = studyMember; - attendance.attendanceStatus = AttendanceStatus.PRESENT; + attendance.attendanceStatus = attendanceStatus; return attendance; } - public static Attendance createLateAttendance(StudySchedule studySchedule, StudyMember studyMember) { - Attendance attendance = new Attendance(); - attendance.studySchedule = studySchedule; - attendance.studyMember = studyMember; - attendance.attendanceStatus = AttendanceStatus.LATE; - return attendance; - } + public static Attendance createWithThreshold(StudySchedule studySchedule, StudyMember studyMember, Integer lateThresholdMinutes, Integer absenceThresholdMinutes) { + LocalDateTime now = LocalDateTime.now(); - public static Attendance create(StudySchedule studySchedule, StudyMember studyMember, AttendanceStatus attendanceStatus) { - Attendance attendance = new Attendance(); - attendance.studySchedule = studySchedule; - attendance.studyMember = studyMember; - attendance.attendanceStatus = attendanceStatus; - return attendance; + if (now.isBefore(studySchedule.getStartDateTime().minusMinutes(lateThresholdMinutes))) { + throw new MosException(AttendanceErrorCode.NOT_PRESENT_TIME); + } + if (now.isAfter(studySchedule.getEndDateTime().plusMinutes(absenceThresholdMinutes))) { + return create(studySchedule, studyMember, AttendanceStatus.UNEXCUSED_ABSENCE); + } + if (now.isAfter(studySchedule.getStartDateTime().plusMinutes(lateThresholdMinutes))) { + return create(studySchedule, studyMember, AttendanceStatus.LATE); + } + return create(studySchedule, studyMember, AttendanceStatus.PRESENT); } public void leaveEarly() { diff --git a/src/main/java/com/mos/backend/common/infrastructure/EntityFacade.java b/src/main/java/com/mos/backend/common/infrastructure/EntityFacade.java index 3806deaf..17edaa6e 100644 --- a/src/main/java/com/mos/backend/common/infrastructure/EntityFacade.java +++ b/src/main/java/com/mos/backend/common/infrastructure/EntityFacade.java @@ -40,6 +40,9 @@ import com.mos.backend.studyschedules.entity.StudySchedule; import com.mos.backend.studyschedules.entity.exception.StudyScheduleErrorCode; import com.mos.backend.studyschedules.infrastructure.StudyScheduleRepository; +import com.mos.backend.studysettings.entity.StudySettings; +import com.mos.backend.studysettings.entity.exception.StudySettingsErrorCode; +import com.mos.backend.studysettings.infrastructure.StudySettingRepository; import com.mos.backend.users.entity.User; import com.mos.backend.users.entity.exception.UserErrorCode; import com.mos.backend.users.infrastructure.respository.UserRepository; @@ -73,6 +76,7 @@ public class EntityFacade { private final PrivateChatRoomMemberRepository privateChatRoomMemberRepository; private final StudyChatRoomRepository studyChatRoomRepository; private final UserStudySettingRepository userStudySettingRepository; + private final StudySettingRepository studySettingRepository; public User getUser(Long userId) { return userRepository.findById(userId) @@ -156,4 +160,9 @@ public PrivateChatRoomMember getPrivateChatRoomMember(Long userId, Long privateC return privateChatRoomMemberRepository.findByUserIdAndPrivateChatRoomId(userId, privateChatRoomId) .orElseThrow(() -> new MosException(PrivateChatRoomMemberErrorCode.NOT_FOUND)); } + + public StudySettings getStudySettings(Long studyId) { + return studySettingRepository.findByStudyId(studyId) + .orElseThrow(() -> new MosException(StudySettingsErrorCode.NOT_FOUND)); + } } diff --git a/src/main/java/com/mos/backend/studyschedules/entity/StudySchedule.java b/src/main/java/com/mos/backend/studyschedules/entity/StudySchedule.java index 5e677baf..d24bbd2e 100644 --- a/src/main/java/com/mos/backend/studyschedules/entity/StudySchedule.java +++ b/src/main/java/com/mos/backend/studyschedules/entity/StudySchedule.java @@ -2,7 +2,14 @@ import com.mos.backend.common.entity.BaseAuditableEntity; import com.mos.backend.studies.entity.Study; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -19,8 +26,6 @@ @Table(name = "study_schedules") public class StudySchedule extends BaseAuditableEntity { - private static final int PRESENT_TIME = 10; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "study_schedule_id") @@ -63,17 +68,4 @@ public void update(String title, String description, LocalDateTime startDateTime this.startDateTime = startDateTime; this.endDateTime = endDateTime; } - - public void complete() { - this.isCompleted = true; - } - - public boolean isPresentTime() { - return LocalDateTime.now().isAfter(startDateTime.minusMinutes(PRESENT_TIME)) - && LocalDateTime.now().isBefore(startDateTime.plusMinutes(PRESENT_TIME)); - } - - public boolean isBeforePresentTime() { - return LocalDateTime.now().isBefore(startDateTime.minusMinutes(PRESENT_TIME)); - } } diff --git a/src/main/java/com/mos/backend/studysettings/entity/StudySettings.java b/src/main/java/com/mos/backend/studysettings/entity/StudySettings.java index d07a9f9e..c73d6af7 100644 --- a/src/main/java/com/mos/backend/studysettings/entity/StudySettings.java +++ b/src/main/java/com/mos/backend/studysettings/entity/StudySettings.java @@ -45,4 +45,4 @@ public void update(Integer lateThresholdMinutes, Integer absenceThresholdMinutes this.lateThresholdMinutes = lateThresholdMinutes; this.absenceThresholdMinutes = absenceThresholdMinutes; } -} \ No newline at end of file +} diff --git a/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java b/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java index 9f84a6ba..634e48fa 100644 --- a/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java +++ b/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java @@ -13,6 +13,7 @@ import com.mos.backend.studyschedules.entity.StudySchedule; import com.mos.backend.studyschedules.entity.exception.StudyScheduleErrorCode; import com.mos.backend.studyschedules.infrastructure.StudyScheduleRepository; +import com.mos.backend.studysettings.entity.StudySettings; import com.mos.backend.users.entity.User; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -22,13 +23,16 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @DisplayName("AttendanceService 테스트") @@ -57,52 +61,21 @@ void createAttendance_Success() { Long studyScheduleId = 1L; User user = mock(User.class); Study study = mock(Study.class); + StudySettings studySettings = mock(StudySettings.class); StudySchedule studySchedule = mock(StudySchedule.class); StudyMember studyMember = mock(StudyMember.class); when(entityFacade.getUser(userId)).thenReturn(user); when(entityFacade.getStudy(studyId)).thenReturn(study); + when(entityFacade.getStudySettings(studyId)).thenReturn(studySettings); + when(studySchedule.getStartDateTime()).thenReturn(LocalDateTime.now()); + when(studySchedule.getEndDateTime()).thenReturn(LocalDateTime.now()); when(entityFacade.getStudySchedule(studyScheduleId)).thenReturn(studySchedule); when(studyMemberRepository.findByUserIdAndStudyId(userId, studyId)).thenReturn(Optional.of(studyMember)); - when(studySchedule.isBeforePresentTime()).thenReturn(false); when(study.getId()).thenReturn(studyId); when(user.getId()).thenReturn(userId); when(studySchedule.getStudy()).thenReturn(study); when(study.isRelated(studyId)).thenReturn(true); - when(studySchedule.isPresentTime()).thenReturn(true); - - // When - attendanceService.create(userId, studyId, studyScheduleId); - - // Then - verify(entityFacade).getUser(userId); - verify(entityFacade).getStudy(studyId); - verify(entityFacade).getStudySchedule(studyScheduleId); - verify(studyMemberRepository).findByUserIdAndStudyId(userId, studyId); - verify(attendanceRepository).save(any()); - } - - @Test - @DisplayName("지각 생성 성공") - void createLateAttendance_Success() { - // Given - Long userId = 1L; - Long studyId = 1L; - Long studyScheduleId = 1L; - User user = mock(User.class); - Study study = mock(Study.class); - StudySchedule studySchedule = mock(StudySchedule.class); - StudyMember studyMember = mock(StudyMember.class); - - when(entityFacade.getUser(userId)).thenReturn(user); - when(entityFacade.getStudy(studyId)).thenReturn(study); - when(entityFacade.getStudySchedule(studyScheduleId)).thenReturn(studySchedule); - when(studyMemberRepository.findByUserIdAndStudyId(userId, studyId)).thenReturn(Optional.of(studyMember)); - when(study.getId()).thenReturn(studyId); - when(user.getId()).thenReturn(userId); - when(studySchedule.getStudy()).thenReturn(study); - when(study.isRelated(studyId)).thenReturn(true); - when(studySchedule.isPresentTime()).thenReturn(false); // When attendanceService.create(userId, studyId, studyScheduleId); @@ -119,34 +92,6 @@ void createLateAttendance_Success() { @Nested @DisplayName("출석 생성 실패 시나리오") class CreateAttendanceFailScenario { - @Test - @DisplayName("스터디 일정의 시작 시간 이전에 출석을 시도한 경우 MosException 발생") - void createAttendance_Fail_BeforePresentTime() { - // Given - Long userId = 1L; - Long studyId = 1L; - Long studyScheduleId = 1L; - User user = mock(User.class); - Study study = mock(Study.class); - StudySchedule studySchedule = mock(StudySchedule.class); - - when(entityFacade.getUser(userId)).thenReturn(user); - when(entityFacade.getStudy(studyId)).thenReturn(study); - when(entityFacade.getStudySchedule(studyScheduleId)).thenReturn(studySchedule); - when(user.getId()).thenReturn(userId); - when(study.getId()).thenReturn(studyId); - when(studyMemberRepository.findByUserIdAndStudyId(userId, studyId)).thenReturn(Optional.of(mock(StudyMember.class))); - when(studySchedule.getStudy()).thenReturn(study); - when(study.isRelated(studyId)).thenReturn(true); - when(studySchedule.isBeforePresentTime()).thenReturn(true); - - // When - MosException exception = assertThrows(MosException.class, () -> attendanceService.create(userId, studyId, studyScheduleId)); - - // Then - assertEquals(exception.getErrorCode(), AttendanceErrorCode.NOT_PRESENT_TIME); - } - @Test @DisplayName("스터디 스케줄이 완료된 경우 MosException 발생") void createAttendance_Fail_StudyScheduleCompleted() { diff --git a/src/test/java/com/mos/backend/attendances/entity/AttendanceTest.java b/src/test/java/com/mos/backend/attendances/entity/AttendanceTest.java new file mode 100644 index 00000000..ff1b011e --- /dev/null +++ b/src/test/java/com/mos/backend/attendances/entity/AttendanceTest.java @@ -0,0 +1,98 @@ +package com.mos.backend.attendances.entity; + +import com.mos.backend.attendances.entity.exception.AttendanceErrorCode; +import com.mos.backend.common.exception.MosException; +import com.mos.backend.studymembers.entity.StudyMember; +import com.mos.backend.studyschedules.entity.StudySchedule; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@DisplayName("Attendance 생성 시 상태 테스트") +class AttendanceTest { + private static final StudySchedule STUDY_SCHEDULE = mock(StudySchedule.class); + private static final StudyMember STUDY_MEMBER = mock(StudyMember.class); + private static final Integer LATE_THRESHOLD = 10; + private static final Integer ABSENCE_THRESHOLD = 20; + + @Nested + @DisplayName("createWithThreshold 시나리오") + class CreateWithThresholdScenario { + @Nested + @DisplayName("성공 시나리오") + class SuccessScenario { + @Test + @DisplayName("정상 출석 상태 저장") + void presentSuccess() { + // given + LocalDateTime now = LocalDateTime.now(); + LocalDateTime start = now.minusMinutes(5); + LocalDateTime end = now.plusMinutes(30); + given(STUDY_SCHEDULE.getStartDateTime()).willReturn(start); + given(STUDY_SCHEDULE.getEndDateTime()).willReturn(end); + // when + Attendance attendance = Attendance.createWithThreshold(STUDY_SCHEDULE, STUDY_MEMBER, LATE_THRESHOLD, ABSENCE_THRESHOLD); + // then + assertThat(attendance.getAttendanceStatus()).isEqualTo(AttendanceStatus.PRESENT); + } + + @Test + @DisplayName("지각 상태 저장") + void lateSuccess() { + // given + LocalDateTime now = LocalDateTime.now(); + LocalDateTime start = now.minusMinutes(LATE_THRESHOLD + 1); + LocalDateTime end = now.plusMinutes(30); + given(STUDY_SCHEDULE.getStartDateTime()).willReturn(start); + given(STUDY_SCHEDULE.getEndDateTime()).willReturn(end); + // when + Attendance attendance = Attendance.createWithThreshold(STUDY_SCHEDULE, STUDY_MEMBER, LATE_THRESHOLD, ABSENCE_THRESHOLD); + // then + assertThat(attendance.getAttendanceStatus()).isEqualTo(AttendanceStatus.LATE); + } + + @Test + @DisplayName("무단 결석 상태 저장") + void absenceSuccess() { + // given + LocalDateTime now = LocalDateTime.now(); + LocalDateTime start = now.minusMinutes(60); + LocalDateTime end = now.minusMinutes(ABSENCE_THRESHOLD + 1); + given(STUDY_SCHEDULE.getStartDateTime()).willReturn(start); + given(STUDY_SCHEDULE.getEndDateTime()).willReturn(end); + // when + Attendance attendance = Attendance.createWithThreshold(STUDY_SCHEDULE, STUDY_MEMBER, LATE_THRESHOLD, ABSENCE_THRESHOLD); + // then + assertThat(attendance.getAttendanceStatus()).isEqualTo(AttendanceStatus.UNEXCUSED_ABSENCE); + } + } + + @Nested + @DisplayName("실패 시나리오") + class FailScenario { + @Test + @DisplayName("출석 가능 시간이 아닌 경우 예외 발생") + void notPresentTimeFail() { + // given + LocalDateTime now = LocalDateTime.now(); + LocalDateTime start = now.plusMinutes(LATE_THRESHOLD + 1); + LocalDateTime end = now.plusMinutes(30); + given(STUDY_SCHEDULE.getStartDateTime()).willReturn(start); + given(STUDY_SCHEDULE.getEndDateTime()).willReturn(end); + // when + MosException ex = assertThrows( + MosException.class, () -> Attendance.createWithThreshold(STUDY_SCHEDULE, STUDY_MEMBER, LATE_THRESHOLD, ABSENCE_THRESHOLD) + ); + // then + assertThat(ex.getErrorCode()).isEqualTo(AttendanceErrorCode.NOT_PRESENT_TIME); + } + } + } +} diff --git a/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java b/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java index 6e4e714d..5dcfdbd4 100644 --- a/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java +++ b/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java @@ -39,7 +39,7 @@ class AttendanceControllerTest { @Test @DisplayName("출석 성공 문서화") - void create_Success_Documentation() throws Exception { + void create_WithThreshold_WithThresholdTime_Success_Documentation() throws Exception { mockMvc.perform( post("/studies/{studyId}/schedules/{studyScheduleId}/attendances", 1, 1, 1) .contentType(MediaType.APPLICATION_JSON) From adb095d21193d25cad48187930aeaf6aade7059f Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Thu, 9 Oct 2025 20:05:49 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=EC=97=B0=EA=B4=80=EA=B4=80?= =?UTF-8?q?=EA=B3=84=20=EC=BB=AC=EB=9F=BC=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/mos/backend/attendances/entity/Attendance.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mos/backend/attendances/entity/Attendance.java b/src/main/java/com/mos/backend/attendances/entity/Attendance.java index 4845a884..ed62afdd 100644 --- a/src/main/java/com/mos/backend/attendances/entity/Attendance.java +++ b/src/main/java/com/mos/backend/attendances/entity/Attendance.java @@ -35,7 +35,7 @@ public class Attendance extends BaseTimeEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "study_id", nullable = false) + @JoinColumn(name = "study_schedule_id", nullable = false) @OnDelete(action = CASCADE) private StudySchedule studySchedule; From 6cc0353092db579669f8b32f85ed7676e59500b1 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Thu, 9 Oct 2025 22:38:28 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=B6=9C=EC=84=9D=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - projection을 통한 쿼리 추가 발생 방지 적용 --- .../application/StudyScheduleService.java | 32 ++++++++++------ .../application/res/StudyScheduleRes.java | 25 ++++++++---- .../StudyScheduleJpaRepository.java | 20 ++++++++-- .../StudyScheduleRepository.java | 7 +++- .../StudyScheduleRepositoryImpl.java | 12 ++++-- .../dto/StudyScheduleWithAttendanceDto.java | 20 ++++++++++ .../api/StudyScheduleController.java | 4 +- .../application/StudyScheduleServiceTest.java | 38 ++++++++++++------- .../StudyScheduleRepositoryTest.java | 10 ++--- 9 files changed, 121 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/mos/backend/studyschedules/infrastructure/dto/StudyScheduleWithAttendanceDto.java diff --git a/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java b/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java index fefdc4b2..8c3c399a 100644 --- a/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java +++ b/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java @@ -11,6 +11,7 @@ import com.mos.backend.studyschedules.entity.StudySchedule; import com.mos.backend.studyschedules.entity.exception.StudyScheduleErrorCode; import com.mos.backend.studyschedules.infrastructure.StudyScheduleRepository; +import com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto; import com.mos.backend.studyschedules.presentation.req.StudyScheduleCreateReq; import com.mos.backend.studyschedules.presentation.req.StudyScheduleUpdateReq; import com.mos.backend.users.entity.User; @@ -56,29 +57,38 @@ private static void validateEndDateTime(LocalDateTime startDateTime, LocalDateTi public List getMyStudySchedules(Long userId) { User user = entityFacade.getUser(userId); - List studySchedules = studyScheduleRepository.findAllByActivatedUserId(user.getId()); + List studySchedules = studyScheduleRepository.findAllByActivatedUserId(user.getId()); return convertToRes(studySchedules); } @PreAuthorize("@studySecurity.isMemberOrAdmin(#studyId)") @Transactional(readOnly = true) - public List getStudySchedules(Long studyId) { + public List getStudySchedules(Long userId, Long studyId) { Study study = entityFacade.getStudy(studyId); - List studySchedules = studyScheduleRepository.findByStudyId(study.getId()); + List studySchedules = studyScheduleRepository.findByStudyIdWithAttendance(userId, study.getId()); return convertToRes(studySchedules); } - private List convertToRes(List studySchedules) { - return studySchedules.stream().map(studySchedule -> { - List studyCurriculumResList = studyCurriculumRepository.findAllByStudyScheduleId(studySchedule.getId()).stream() - .map(StudyCurriculumRes::from) - .toList(); - return StudyScheduleRes.of(studySchedule, studyCurriculumResList); - }).toList(); + private List convertToRes(List dtos) { + return dtos.stream().map(dto -> { + List studyCurriculumResList = studyCurriculumRepository.findAllByStudyScheduleId(dto.getStudyScheduleId()).stream() + .map(StudyCurriculumRes::from) + .toList(); + return StudyScheduleRes.of( + dto.getStudyScheduleId(), + dto.getTitle(), + dto.getDescription(), + dto.getStartDateTime(), + dto.getEndDateTime(), + dto.getStudyId(), + Objects.isNull(dto.getAttendanceStatus()) ? null : dto.getAttendanceStatus().getDescription(), + studyCurriculumResList + ); + }) + .toList(); } - @PreAuthorize("@studySecurity.isLeaderOrAdmin(#studyId)") @Transactional public void updateStudySchedule(Long studyId, Long studyScheduleId, StudyScheduleUpdateReq req) { diff --git a/src/main/java/com/mos/backend/studyschedules/application/res/StudyScheduleRes.java b/src/main/java/com/mos/backend/studyschedules/application/res/StudyScheduleRes.java index 609213c6..96f69dd8 100644 --- a/src/main/java/com/mos/backend/studyschedules/application/res/StudyScheduleRes.java +++ b/src/main/java/com/mos/backend/studyschedules/application/res/StudyScheduleRes.java @@ -1,6 +1,5 @@ package com.mos.backend.studyschedules.application.res; -import com.mos.backend.studyschedules.entity.StudySchedule; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,16 +17,26 @@ public class StudyScheduleRes { private Long studyId; + private String attendanceStatusDescription; + private List studyCurriculumResList; - public static StudyScheduleRes of(StudySchedule studySchedule, List studyCurriculumList) { + public static StudyScheduleRes of(Long studyScheduleId, + String title, + String description, + LocalDateTime startDateTime, + LocalDateTime endDateTime, + Long studyId, + String attendanceStatusDescription, + List studyCurriculumList) { return new StudyScheduleRes( - studySchedule.getId(), - studySchedule.getTitle(), - studySchedule.getDescription(), - studySchedule.getStartDateTime(), - studySchedule.getEndDateTime(), - studySchedule.getStudy().getId(), + studyScheduleId, + title, + description, + startDateTime, + endDateTime, + studyId, + attendanceStatusDescription, studyCurriculumList ); } diff --git a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java index 06ec8b2f..f23e061e 100644 --- a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java +++ b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java @@ -1,20 +1,34 @@ package com.mos.backend.studyschedules.infrastructure; import com.mos.backend.studyschedules.entity.StudySchedule; +import com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; public interface StudyScheduleJpaRepository extends JpaRepository { - List findByStudyId(Long studyId); + @Query(""" + SELECT new com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto( + ss.id, ss.title, ss.description, ss.startDateTime, ss.endDateTime, ss.isCompleted, ss.study.id, a.attendanceStatus + ) + FROM StudySchedule ss + LEFT JOIN Attendance a ON a.studySchedule.id = ss.id AND a.studyMember.user.id = :userId + WHERE ss.study.id = :studyId + """) + List findByStudyIdWithAttendance(Long userId, Long studyId); @Query(""" - SELECT ss + SELECT new com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto( + ss.id, ss.title, ss.description, ss.startDateTime, ss.endDateTime, ss.isCompleted, ss.study.id, a.attendanceStatus + ) FROM StudySchedule ss JOIN ss.study s JOIN StudyMember sm ON sm.study = s + LEFT JOIN Attendance a ON a.studySchedule.id = ss.id AND a.studyMember.user.id = :userId WHERE sm.user.id = :userId AND sm.status = 'ACTIVATED' """) - List findAllByActivatedUserId(Long userId); + List findAllByActivatedUserId(Long userId); + + List findByStudyId(Long studyId); } diff --git a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java index 3c6c0224..96bb33d8 100644 --- a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java +++ b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java @@ -1,6 +1,7 @@ package com.mos.backend.studyschedules.infrastructure; import com.mos.backend.studyschedules.entity.StudySchedule; +import com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto; import java.util.List; import java.util.Optional; @@ -8,11 +9,13 @@ public interface StudyScheduleRepository { StudySchedule save(StudySchedule studySchedule); - List findByStudyId(Long studyId); + List findByStudyIdWithAttendance(Long userId, Long studyId); - List findAllByActivatedUserId(Long userId); + List findAllByActivatedUserId(Long userId); Optional findById(Long studyScheduleId); void delete(StudySchedule studySchedule); + + List findByStudyId(Long studyId); } diff --git a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java index e1777297..40bbb5e0 100644 --- a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java +++ b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java @@ -1,6 +1,7 @@ package com.mos.backend.studyschedules.infrastructure; import com.mos.backend.studyschedules.entity.StudySchedule; +import com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -19,12 +20,12 @@ public StudySchedule save(StudySchedule studySchedule) { } @Override - public List findByStudyId(Long studyId) { - return studyScheduleJpaRepository.findByStudyId(studyId); + public List findByStudyIdWithAttendance(Long userId, Long studyId) { + return studyScheduleJpaRepository.findByStudyIdWithAttendance(userId, studyId); } @Override - public List findAllByActivatedUserId(Long userId) { + public List findAllByActivatedUserId(Long userId) { return studyScheduleJpaRepository.findAllByActivatedUserId(userId); } @@ -37,4 +38,9 @@ public Optional findById(Long studyScheduleId) { public void delete(StudySchedule studySchedule) { studyScheduleJpaRepository.delete(studySchedule); } + + @Override + public List findByStudyId(Long studyId) { + return studyScheduleJpaRepository.findByStudyId(studyId); + } } diff --git a/src/main/java/com/mos/backend/studyschedules/infrastructure/dto/StudyScheduleWithAttendanceDto.java b/src/main/java/com/mos/backend/studyschedules/infrastructure/dto/StudyScheduleWithAttendanceDto.java new file mode 100644 index 00000000..864bcced --- /dev/null +++ b/src/main/java/com/mos/backend/studyschedules/infrastructure/dto/StudyScheduleWithAttendanceDto.java @@ -0,0 +1,20 @@ +package com.mos.backend.studyschedules.infrastructure.dto; + +import com.mos.backend.attendances.entity.AttendanceStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class StudyScheduleWithAttendanceDto { + private Long studyScheduleId; + private String title; + private String description; + private LocalDateTime startDateTime; + private LocalDateTime endDateTime; + private boolean isCompleted; + private Long studyId; + private AttendanceStatus attendanceStatus; +} diff --git a/src/main/java/com/mos/backend/studyschedules/presentation/controller/api/StudyScheduleController.java b/src/main/java/com/mos/backend/studyschedules/presentation/controller/api/StudyScheduleController.java index 73ee5403..5462e8af 100644 --- a/src/main/java/com/mos/backend/studyschedules/presentation/controller/api/StudyScheduleController.java +++ b/src/main/java/com/mos/backend/studyschedules/presentation/controller/api/StudyScheduleController.java @@ -32,8 +32,8 @@ public List getMyStudySchedules(@AuthenticationPrincipal Long @ResponseStatus(HttpStatus.OK) @GetMapping("/studies/{studyId}/schedules") - public List getStudySchedules(@PathVariable Long studyId) { - return studyScheduleService.getStudySchedules(studyId); + public List getStudySchedules(@AuthenticationPrincipal Long userId, @PathVariable Long studyId) { + return studyScheduleService.getStudySchedules(userId, studyId); } @ResponseStatus(HttpStatus.OK) diff --git a/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java b/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java index 8773a36c..c75e4ebb 100644 --- a/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java +++ b/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java @@ -11,9 +11,11 @@ import com.mos.backend.studyschedulecurriculums.infrastructure.StudyScheduleCurriculumRepository; import com.mos.backend.studyschedules.entity.StudySchedule; import com.mos.backend.studyschedules.infrastructure.StudyScheduleRepository; +import com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto; import com.mos.backend.studyschedules.presentation.req.StudyScheduleCreateReq; import com.mos.backend.studyschedules.presentation.req.StudyScheduleUpdateReq; import com.mos.backend.users.entity.User; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -26,7 +28,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @DisplayName("StudyScheduleService 테스트") @@ -101,23 +109,29 @@ void validateEndDateTime_Fail() { @Nested @DisplayName("스터디 일정 조회 성공 시나리오") class GetStudyScheduleSuccessScenario { + private static StudyScheduleWithAttendanceDto dto = mock(StudyScheduleWithAttendanceDto.class); + private static List dtos = List.of(dto); + private static final Long STUDY_SCHEDULE_ID = 1L; + + @BeforeEach + void setUp() { + when(dto.getStudyScheduleId()).thenReturn(STUDY_SCHEDULE_ID); + } + @Test @DisplayName("내 스터디 일정 조회 성공") void getMyStudySchedules_Success() { // Given Long userId = 1L; User user = mock(User.class); - Study study = mock(Study.class); StudySchedule studySchedule = mock(StudySchedule.class); List studySchedules = List.of(studySchedule); List studyCurriculums = List.of(mock(StudyCurriculum.class)); when(entityFacade.getUser(userId)).thenReturn(user); when(user.getId()).thenReturn(userId); - when(studyScheduleRepository.findAllByActivatedUserId(userId)).thenReturn(studySchedules); - when(studySchedule.getStudy()).thenReturn(study); - when(studySchedule.getId()).thenReturn(1L); - when(studyCurriculumRepository.findAllByStudyScheduleId(studySchedule.getId())).thenReturn(studyCurriculums); + when(studyScheduleRepository.findAllByActivatedUserId(userId)).thenReturn(dtos); + when(studyCurriculumRepository.findAllByStudyScheduleId(STUDY_SCHEDULE_ID)).thenReturn(studyCurriculums); // When studyScheduleService.getMyStudySchedules(userId); @@ -125,7 +139,7 @@ void getMyStudySchedules_Success() { // Then verify(entityFacade).getUser(userId); verify(studyScheduleRepository).findAllByActivatedUserId(userId); - verify(studyCurriculumRepository, times(studySchedules.size())).findAllByStudyScheduleId(anyLong()); + verify(studyCurriculumRepository, times(studySchedules.size())).findAllByStudyScheduleId(STUDY_SCHEDULE_ID); } @Test @@ -141,17 +155,15 @@ void getStudySchedules_Success() { when(entityFacade.getStudy(studyId)).thenReturn(study); when(study.getId()).thenReturn(studyId); - when(studyScheduleRepository.findByStudyId(studyId)).thenReturn(studySchedules); - when(studySchedule.getStudy()).thenReturn(study); - when(studySchedule.getId()).thenReturn(1L); - when(studyCurriculumRepository.findAllByStudyScheduleId(studySchedule.getId())).thenReturn(studyCurriculums); + when(studyScheduleRepository.findByStudyIdWithAttendance(userId, studyId)).thenReturn(dtos); + when(studyCurriculumRepository.findAllByStudyScheduleId(STUDY_SCHEDULE_ID)).thenReturn(studyCurriculums); // When - studyScheduleService.getStudySchedules(studyId); + studyScheduleService.getStudySchedules(userId, studyId); // Then verify(entityFacade).getStudy(studyId); - verify(studyScheduleRepository).findByStudyId(studyId); + verify(studyScheduleRepository).findByStudyIdWithAttendance(userId, studyId); verify(studyCurriculumRepository, times(studySchedules.size())).findAllByStudyScheduleId(anyLong()); } } diff --git a/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java b/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java index cbc05b5b..dd7a305c 100644 --- a/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java +++ b/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java @@ -2,7 +2,7 @@ import com.mos.backend.common.EntitySaver; import com.mos.backend.studies.entity.Study; -import com.mos.backend.studyschedules.entity.StudySchedule; +import com.mos.backend.studyschedules.infrastructure.dto.StudyScheduleWithAttendanceDto; import com.mos.backend.testconfig.AbstractTestContainer; import com.mos.backend.users.entity.User; import org.junit.jupiter.api.DisplayName; @@ -39,12 +39,12 @@ void findAllByActivatedUserIdTest() { entitySaver.saveStudySchedule(study2); // When - List studySchedules = studyScheduleRepository.findAllByActivatedUserId(user.getId()); + List dtos = studyScheduleRepository.findAllByActivatedUserId(user.getId()); // Then - assertThat(studySchedules).hasSize(2); - assertThat(studySchedules) - .extracting(studySchedule -> studySchedule.getStudy().getId()) + assertThat(dtos).hasSize(2); + assertThat(dtos) + .extracting(dto -> dto.getStudyId()) .containsExactlyInAnyOrder(study1.getId(), study2.getId()); } } From 46e60ad05edcc651b71cf44a8a0d5b0d2612abe8 Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Thu, 9 Oct 2025 22:39:57 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EC=BF=BC=EB=A6=AC=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyschedules/application/StudyScheduleService.java | 2 +- .../infrastructure/StudyScheduleJpaRepository.java | 2 +- .../infrastructure/StudyScheduleRepository.java | 2 +- .../infrastructure/StudyScheduleRepositoryImpl.java | 4 ++-- .../application/StudyScheduleServiceTest.java | 4 ++-- .../infrastructure/StudyScheduleRepositoryTest.java | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java b/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java index 8c3c399a..0741f8a3 100644 --- a/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java +++ b/src/main/java/com/mos/backend/studyschedules/application/StudyScheduleService.java @@ -57,7 +57,7 @@ private static void validateEndDateTime(LocalDateTime startDateTime, LocalDateTi public List getMyStudySchedules(Long userId) { User user = entityFacade.getUser(userId); - List studySchedules = studyScheduleRepository.findAllByActivatedUserId(user.getId()); + List studySchedules = studyScheduleRepository.findAllByUserIdAndActivated(user.getId()); return convertToRes(studySchedules); } diff --git a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java index f23e061e..a2cd535c 100644 --- a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java +++ b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleJpaRepository.java @@ -28,7 +28,7 @@ public interface StudyScheduleJpaRepository extends JpaRepository findAllByActivatedUserId(Long userId); + List findAllByUserIdAndActivated(Long userId); List findByStudyId(Long studyId); } diff --git a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java index 96bb33d8..6fd23099 100644 --- a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java +++ b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepository.java @@ -11,7 +11,7 @@ public interface StudyScheduleRepository { List findByStudyIdWithAttendance(Long userId, Long studyId); - List findAllByActivatedUserId(Long userId); + List findAllByUserIdAndActivated(Long userId); Optional findById(Long studyScheduleId); diff --git a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java index 40bbb5e0..59036789 100644 --- a/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java +++ b/src/main/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryImpl.java @@ -25,8 +25,8 @@ public List findByStudyIdWithAttendance(Long use } @Override - public List findAllByActivatedUserId(Long userId) { - return studyScheduleJpaRepository.findAllByActivatedUserId(userId); + public List findAllByUserIdAndActivated(Long userId) { + return studyScheduleJpaRepository.findAllByUserIdAndActivated(userId); } @Override diff --git a/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java b/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java index c75e4ebb..0f21bbe6 100644 --- a/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java +++ b/src/test/java/com/mos/backend/studyschedules/application/StudyScheduleServiceTest.java @@ -130,7 +130,7 @@ void getMyStudySchedules_Success() { when(entityFacade.getUser(userId)).thenReturn(user); when(user.getId()).thenReturn(userId); - when(studyScheduleRepository.findAllByActivatedUserId(userId)).thenReturn(dtos); + when(studyScheduleRepository.findAllByUserIdAndActivated(userId)).thenReturn(dtos); when(studyCurriculumRepository.findAllByStudyScheduleId(STUDY_SCHEDULE_ID)).thenReturn(studyCurriculums); // When @@ -138,7 +138,7 @@ void getMyStudySchedules_Success() { // Then verify(entityFacade).getUser(userId); - verify(studyScheduleRepository).findAllByActivatedUserId(userId); + verify(studyScheduleRepository).findAllByUserIdAndActivated(userId); verify(studyCurriculumRepository, times(studySchedules.size())).findAllByStudyScheduleId(STUDY_SCHEDULE_ID); } diff --git a/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java b/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java index dd7a305c..65aad0e4 100644 --- a/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java +++ b/src/test/java/com/mos/backend/studyschedules/infrastructure/StudyScheduleRepositoryTest.java @@ -26,9 +26,9 @@ class StudyScheduleRepositoryTest extends AbstractTestContainer { private StudyScheduleRepository studyScheduleRepository; @Test - @DisplayName("findAllByActivatedUserId 테스트") + @DisplayName("findAllByUserIdAndActivated 테스트") @DirtiesContext - void findAllByActivatedUserIdTest() { + void findAllByUserIdAndActivatedTest() { // Given User user = entitySaver.saveUser(); Study study1 = entitySaver.saveStudy(); @@ -39,7 +39,7 @@ void findAllByActivatedUserIdTest() { entitySaver.saveStudySchedule(study2); // When - List dtos = studyScheduleRepository.findAllByActivatedUserId(user.getId()); + List dtos = studyScheduleRepository.findAllByUserIdAndActivated(user.getId()); // Then assertThat(dtos).hasSize(2); From 7da8c625e1153ea7a098efb0ba697d10822397ed Mon Sep 17 00:00:00 2001 From: seungheonlee Date: Mon, 20 Oct 2025 00:03:36 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20Req=EC=97=90=EC=84=9C=20String?= =?UTF-8?q?=EC=9D=84=20enum=EC=9C=BC=EB=A1=9C=20=EB=B0=9B=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/attendances/application/AttendanceService.java | 2 +- .../attendances/presentation/req/AttendanceUpdateReq.java | 3 ++- .../attendances/application/AttendanceServiceTest.java | 4 ++-- .../controller/api/AttendanceControllerTest.java | 7 +++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/mos/backend/attendances/application/AttendanceService.java b/src/main/java/com/mos/backend/attendances/application/AttendanceService.java index 1941dc75..e82a73bb 100644 --- a/src/main/java/com/mos/backend/attendances/application/AttendanceService.java +++ b/src/main/java/com/mos/backend/attendances/application/AttendanceService.java @@ -70,7 +70,7 @@ public void update(Long userId, Long studyId, Long studyScheduleId, AttendanceUp StudyMember studyMember = studyMemberRepository.findByUserIdAndStudyId(user.getId(), study.getId()) .orElseThrow(() -> new MosException(StudyMemberErrorCode.STUDY_MEMBER_NOT_FOUND)); - AttendanceStatus attendanceStatus = AttendanceStatus.fromDescription(req.getAttendanceStatus()); + AttendanceStatus attendanceStatus = req.getAttendanceStatus(); if (!attendanceStatus.isModifiable()) throw new MosException(AttendanceErrorCode.UNMODIFIABLE_STATUS); diff --git a/src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java b/src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java index 288fbf77..734d6b32 100644 --- a/src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java +++ b/src/main/java/com/mos/backend/attendances/presentation/req/AttendanceUpdateReq.java @@ -1,5 +1,6 @@ package com.mos.backend.attendances.presentation.req; +import com.mos.backend.attendances.entity.AttendanceStatus; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,5 +9,5 @@ @NoArgsConstructor @AllArgsConstructor public class AttendanceUpdateReq { - private String attendanceStatus; + private AttendanceStatus attendanceStatus; } diff --git a/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java b/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java index 634e48fa..d89ec118 100644 --- a/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java +++ b/src/test/java/com/mos/backend/attendances/application/AttendanceServiceTest.java @@ -166,7 +166,7 @@ void updateAttendance_Success() { StudyMember studyMember = mock(StudyMember.class); Attendance attendance = mock(Attendance.class); Optional optionalAttendance = Optional.of(attendance); - AttendanceUpdateReq req = new AttendanceUpdateReq(AttendanceStatus.EARLY_LEAVE.getDescription()); + AttendanceUpdateReq req = new AttendanceUpdateReq(AttendanceStatus.EARLY_LEAVE); when(entityFacade.getUser(userId)).thenReturn(user); when(entityFacade.getStudy(studyId)).thenReturn(study); @@ -207,7 +207,7 @@ void updateAttendance_Fail() { StudyMember studyMember = mock(StudyMember.class); Attendance attendance = mock(Attendance.class); Optional optionalAttendance = Optional.of(attendance); - AttendanceUpdateReq req = new AttendanceUpdateReq(AttendanceStatus.EARLY_LEAVE.getDescription()); + AttendanceUpdateReq req = new AttendanceUpdateReq(AttendanceStatus.EARLY_LEAVE); when(entityFacade.getUser(userId)).thenReturn(user); when(entityFacade.getStudy(studyId)).thenReturn(study); diff --git a/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java b/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java index 5dcfdbd4..1d7bb089 100644 --- a/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java +++ b/src/test/java/com/mos/backend/attendances/presentation/controller/api/AttendanceControllerTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.mos.backend.attendances.application.AttendanceService; +import com.mos.backend.attendances.entity.AttendanceStatus; +import com.mos.backend.attendances.presentation.req.AttendanceUpdateReq; import com.mos.backend.common.jwt.TokenUtil; import com.mos.backend.common.test.config.TestWebSocketConfig; import com.mos.backend.securityuser.WithMockCustomUser; @@ -55,10 +57,11 @@ void create_WithThreshold_WithThresholdTime_Success_Documentation() throws Excep @Test @DisplayName("출석 수정 성공 문서화") void update_Success_Documentation() throws Exception { + AttendanceUpdateReq req = new AttendanceUpdateReq(AttendanceStatus.PRESENT); mockMvc.perform( - put("/studies/{studyId}/schedules/{studyScheduleId}/attendances", 1, 1, 1) + put("/studies/{studyId}/schedules/{studyScheduleId}/attendances", 1, 1) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString("출석")) + .content(objectMapper.writeValueAsString(req)) ) .andExpect(status().isOk()) .andDo(document("update-attendance-success",