From ce726c1d9b1e53673b48ff3ec4bbe3f2a5b2f2ba Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Fri, 10 Oct 2025 10:35:59 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refact:=20record=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EC=8B=9C=EA=B0=84=20=EC=98=A4=EC=A0=84=20?= =?UTF-8?q?0=EC=8B=9C=EB=A1=9C=20+=20validation=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyRecordController.java | 5 +++ .../record/dto/StudyRecordRequestDto.java | 6 ++++ .../record/service/StudyRecordService.java | 19 +++++------- .../controller/StudyRecordControllerTest.java | 31 +------------------ 4 files changed, 20 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java b/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java index 6977a3a0..a57ce7f4 100644 --- a/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java +++ b/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java @@ -5,6 +5,8 @@ import com.back.domain.study.record.service.StudyRecordService; import com.back.global.common.dto.RsData; import com.back.global.security.user.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; @@ -18,12 +20,15 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/plans/records") +@Tag(name = "StudyPlan", description = "학습 계획 관련 API") public class StudyRecordController { private final StudyRecordService studyRecordService; // ======================= 생성 ====================== // 학습 기록 생성 @PostMapping + @Operation( summary = "학습 기록 생성", + description = "기록을 생성합니다.") public ResponseEntity> createStudyRecord( @AuthenticationPrincipal CustomUserDetails user, @Valid @RequestBody StudyRecordRequestDto request diff --git a/src/main/java/com/back/domain/study/record/dto/StudyRecordRequestDto.java b/src/main/java/com/back/domain/study/record/dto/StudyRecordRequestDto.java index 6d3f6d27..19778e0c 100644 --- a/src/main/java/com/back/domain/study/record/dto/StudyRecordRequestDto.java +++ b/src/main/java/com/back/domain/study/record/dto/StudyRecordRequestDto.java @@ -1,5 +1,6 @@ package com.back.domain.study.record.dto; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,10 +11,15 @@ @Getter @NoArgsConstructor public class StudyRecordRequestDto { + @NotNull(message = "계획 ID는 필수입니다.") private Long planId; + @NotNull(message = "방 ID는 필수입니다.") private Long roomId; + @NotNull(message = "시작 시간은 필수입니다.") private LocalDateTime startTime; + @NotNull(message = "종료 시간은 필수입니다.") private LocalDateTime endTime; + private Long duration; private List pauseInfos = new ArrayList<>(); diff --git a/src/main/java/com/back/domain/study/record/service/StudyRecordService.java b/src/main/java/com/back/domain/study/record/service/StudyRecordService.java index 9958fc97..74a431b3 100644 --- a/src/main/java/com/back/domain/study/record/service/StudyRecordService.java +++ b/src/main/java/com/back/domain/study/record/service/StudyRecordService.java @@ -54,12 +54,9 @@ public StudyRecordResponseDto createStudyRecord(Long userId, StudyRecordRequestD throw new CustomException(ErrorCode.PLAN_FORBIDDEN); } - // 방 조회 (우선은 옵셔널로 설정) - Room room = null; - if (request.getRoomId() != null) { - room = roomRepository.findById(request.getRoomId()) - .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - } + // 방 조회 (필수) + Room room = roomRepository.findById(request.getRoomId()) + .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); // 일시정지 정보를 엔티티로 생성 List pauseInfos = request.getPauseInfos().stream() @@ -125,9 +122,9 @@ public List getStudyRecordsByDate(Long userId, LocalDate User user = userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - // 오전 4시 기준 하루의 시작과 끝 설정 - LocalDateTime startOfDay = date.atTime(4, 0, 0); - LocalDateTime endOfDay = date.plusDays(1).atTime(4, 0, 0); + // 오전 0시 기준 하루의 시작과 끝 설정 + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.plusDays(1).atStartOfDay(); // 시작~종료 시간을 포함하는 일자의 학습 기록 조회 List records = studyRecordRepository @@ -151,8 +148,8 @@ private void checkAndNotifyDailyGoalAchievement(Long userId, LocalDate date) { // 오늘 완료한 계획 개수 int completedCount = 0; - LocalDateTime startOfDay = date.atTime(4, 0); - LocalDateTime endOfDay = date.plusDays(1).atTime(4, 0); + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.plusDays(1).atStartOfDay(); for (StudyPlan plan : todayPlans) { boolean hasRecord = studyRecordRepository.existsByStudyPlanIdAndDate( diff --git a/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java b/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java index 906572f9..dcf78c3b 100644 --- a/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java +++ b/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java @@ -250,36 +250,7 @@ void t3() throws Exception { } @Test - @DisplayName("학습 기록 조회 - 전날 밤~당일 새벽 기록의 경우") - void t4() throws Exception { - mvc.perform(post("/api/plans/records") - .header("Authorization", "Bearer faketoken") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "planId": %d, - "startTime": "2025-10-02T23:00:00", - "endTime": "2025-10-03T02:00:00", - "duration": 10800, - "pauseInfos": [] - } - """.formatted(singlePlan.getId()))) - .andExpect(status().isOk()); - - // 10월 2일로 조회 (04:00 기준이므로 이 기록이 포함되어야 함) - ResultActions resultActions = mvc.perform(get("/api/plans/records?date=2025-10-02") - .header("Authorization", "Bearer faketoken")) - .andDo(print()); - - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data", hasSize(1))) - .andExpect(jsonPath("$.data[0].startTime").value("2025-10-02T23:00:00")) - .andExpect(jsonPath("$.data[0].endTime").value("2025-10-03T02:00:00")); - } - - @Test - @DisplayName("학습 기록 조회 - 전날 밤~당일 오전 4시 이후 끝난 기록의 경우") + @DisplayName("학습 기록 조회 - 전날 밤 시작 ~ 당일 끝난 기록의 경우") void t5() throws Exception { mvc.perform(post("/api/plans/records") .header("Authorization", "Bearer faketoken") From c5c38ddd3d325f2b78de94d085ac8aedc6699356 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Fri, 10 Oct 2025 10:41:12 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=EC=A3=BC=EC=84=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20import=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/record/controller/StudyRecordController.java | 4 +++- .../domain/study/record/service/StudyRecordService.java | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java b/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java index a57ce7f4..5febec88 100644 --- a/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java +++ b/src/main/java/com/back/domain/study/record/controller/StudyRecordController.java @@ -20,7 +20,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/plans/records") -@Tag(name = "StudyPlan", description = "학습 계획 관련 API") +@Tag(name = "StudyRecord", description = "학습 조회 관련 API") public class StudyRecordController { private final StudyRecordService studyRecordService; @@ -40,6 +40,8 @@ public ResponseEntity> createStudyRecord( // ======================= 조회 ====================== // 일별 학습 기록 조회 @GetMapping + @Operation( summary = "학습 기록 조회", + description = "날짜별 기록을 조회합니다.") public ResponseEntity>> getDailyStudyRecord( @AuthenticationPrincipal CustomUserDetails user, @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date diff --git a/src/main/java/com/back/domain/study/record/service/StudyRecordService.java b/src/main/java/com/back/domain/study/record/service/StudyRecordService.java index 74a431b3..fd81097d 100644 --- a/src/main/java/com/back/domain/study/record/service/StudyRecordService.java +++ b/src/main/java/com/back/domain/study/record/service/StudyRecordService.java @@ -197,15 +197,15 @@ private List getTodayStudyPlans(Long userId, LocalDate date) { // ===================== 유틸 ===================== // 시간 범위 검증 - private void validateTimeRange(java.time.LocalDateTime startTime, java.time.LocalDateTime endTime) { + private void validateTimeRange(LocalDateTime startTime, LocalDateTime endTime) { if (startTime.isAfter(endTime) || startTime.isEqual(endTime)) { throw new CustomException(ErrorCode.INVALID_TIME_RANGE); } } // 일시정지 시간이 학습 시간 내에 있는지 검증 - private void validatePauseInStudyRange(java.time.LocalDateTime studyStart, java.time.LocalDateTime studyEnd, - java.time.LocalDateTime pauseStart, java.time.LocalDateTime pauseEnd) { + private void validatePauseInStudyRange(LocalDateTime studyStart, LocalDateTime studyEnd, + LocalDateTime pauseStart, LocalDateTime pauseEnd) { if (pauseStart.isBefore(studyStart) || pauseEnd.isAfter(studyEnd)) { throw new CustomException(ErrorCode.INVALID_TIME_RANGE); } From b31ded21619affa3332b30849864c57b86bfec65 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Fri, 10 Oct 2025 11:01:14 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refact:=20roomId=20=ED=95=84=EC=88=98=20+?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyRecordControllerTest.java | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java b/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java index dcf78c3b..bbdf08ce 100644 --- a/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java +++ b/src/test/java/com/back/domain/study/record/controller/StudyRecordControllerTest.java @@ -5,6 +5,8 @@ import com.back.domain.study.plan.entity.RepeatRule; import com.back.domain.study.plan.entity.StudyPlan; import com.back.domain.study.plan.repository.StudyPlanRepository; +import com.back.domain.studyroom.entity.Room; +import com.back.domain.studyroom.repository.RoomRepository; import com.back.domain.user.entity.Role; import com.back.domain.user.entity.User; import com.back.domain.user.entity.UserStatus; @@ -53,8 +55,11 @@ class StudyRecordControllerTest { private StudyPlanRepository studyPlanRepository; @Autowired private UserRepository userRepository; + @Autowired + private RoomRepository roomRepository; private User testUser; + private Room testRoom; private StudyPlan singlePlan; private StudyPlan dailyPlan; @@ -73,6 +78,7 @@ void setUp() { singlePlan = createSinglePlan(); dailyPlan = createDailyPlan(); + testRoom = createRoom(testUser); } private void setupJwtMock(User user) { @@ -115,7 +121,23 @@ private StudyPlan createDailyPlan() { plan.setRepeatRule(repeatRule); return studyPlanRepository.save(plan); + } + + private Room createRoom(User owner) { + + testRoom = Room.create( + "코딩테스트 준비", + "알고리즘 문제 풀이 및 코드 리뷰", + false, // 공개방 + null, // 비밀번호 없음 + 20, // 최대 20명 + owner, + null, // 테마 없음 + true // WebRTC 활성화 + ); + testRoom = roomRepository.save(testRoom); + return testRoom; } @Test @@ -127,12 +149,13 @@ void t1() throws Exception { .content(""" { "planId": %d, + "roomId": %d, "startTime": "2025-10-03T10:00:00", "endTime": "2025-10-03T12:00:00", "duration": 7200, "pauseInfos": [] } - """.formatted(singlePlan.getId()))) + """.formatted(singlePlan.getId(), testRoom.getId()))) .andDo(print()); resultActions @@ -142,6 +165,7 @@ void t1() throws Exception { .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.message").value("학습 기록이 생성되었습니다.")) .andExpect(jsonPath("$.data.planId").value(singlePlan.getId())) + .andExpect(jsonPath("$.data.roomId").value(testRoom.getId())) .andExpect(jsonPath("$.data.startTime").value("2025-10-03T10:00:00")) .andExpect(jsonPath("$.data.endTime").value("2025-10-03T12:00:00")) .andExpect(jsonPath("$.data.duration").value(7200)) @@ -156,6 +180,7 @@ void t2() throws Exception { .content(""" { "planId": %d, + "roomId": %d, "startTime": "2025-10-03T14:00:00", "endTime": "2025-10-03T17:00:00", "duration": "7500", @@ -172,7 +197,7 @@ void t2() throws Exception { } ] } - """.formatted(dailyPlan.getId()))) + """.formatted(singlePlan.getId(), testRoom.getId()))) .andDo(print()); resultActions @@ -192,6 +217,7 @@ void t2_1() throws Exception { .content(""" { "planId": %d, + "roomId": %d, "startTime": "2025-10-03T14:00:00", "endTime": "2025-10-03T17:00:00", "duration": "7200", @@ -207,7 +233,7 @@ void t2_1() throws Exception { } ] } - """.formatted(dailyPlan.getId()))) + """.formatted(singlePlan.getId(), testRoom.getId()))) .andDo(print()); resultActions @@ -227,12 +253,13 @@ void t3() throws Exception { .content(""" { "planId": %d, + "roomId": %d, "startTime": "2025-10-03T10:00:00", "endTime": "2025-10-03T12:00:00", "duration": 7200, "pauseInfos": [] } - """.formatted(singlePlan.getId()))) + """.formatted(singlePlan.getId(), testRoom.getId()))) .andExpect(status().isOk()); // 조회 @@ -258,12 +285,13 @@ void t5() throws Exception { .content(""" { "planId": %d, + "roomId": %d, "startTime": "2025-10-02T23:00:00", "endTime": "2025-10-03T05:00:00", "duration": 21600, "pauseInfos": [] } - """.formatted(singlePlan.getId()))) + """.formatted(singlePlan.getId(), testRoom.getId()))) .andExpect(status().isOk()); // 10월 2일 조회 ResultActions resultActions = mvc.perform(get("/api/plans/records?date=2025-10-02") From 3c7056f8fd61d3d17851ada2aa9795f81f5cba1d Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Fri, 10 Oct 2025 11:05:39 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refact:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/back/domain/study/record/entity/StudyRecord.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/back/domain/study/record/entity/StudyRecord.java b/src/main/java/com/back/domain/study/record/entity/StudyRecord.java index 756af318..57b4ad49 100644 --- a/src/main/java/com/back/domain/study/record/entity/StudyRecord.java +++ b/src/main/java/com/back/domain/study/record/entity/StudyRecord.java @@ -29,18 +29,20 @@ public class StudyRecord extends BaseEntity { private StudyPlan studyPlan; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") + @JoinColumn(name = "room_id", nullable = false) private Room room; // 초 단위 @Column(nullable = false) private Long duration; + @Column(nullable = false) private LocalDateTime startTime; @OneToMany(mappedBy = "studyRecord", cascade = CascadeType.ALL, orphanRemoval = true) private List pauseInfos = new ArrayList<>(); + @Column(nullable = false) private LocalDateTime endTime; public static StudyRecord create(User user, StudyPlan studyPlan, Room room,