From 00191bc64aee233f722d5bc7d880545ec42957a1 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Tue, 30 Sep 2025 10:16:34 +0900 Subject: [PATCH 1/6] =?UTF-8?q?style:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/plan/service/StudyPlanService.java | 3 ++- .../study/todo/controller/TodoController.java | 14 ++++++++++++++ .../com/back/domain/study/todo/entity/Todo.java | 3 ++- .../study/todo/repository/TodoRepository.java | 9 +++++++++ .../domain/study/todo/service/TodoService.java | 7 +++++++ 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/back/domain/study/todo/controller/TodoController.java create mode 100644 src/main/java/com/back/domain/study/todo/repository/TodoRepository.java create mode 100644 src/main/java/com/back/domain/study/todo/service/TodoService.java diff --git a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java index b53966b2..5faa87d5 100644 --- a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java +++ b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java @@ -548,7 +548,7 @@ private void validateUserAccess(StudyPlan studyPlan, Long userId) { throw new CustomException(ErrorCode.FORBIDDEN); } } - + // 시작, 종료 날짜 검증 private void validateDateTime(LocalDateTime startDate, LocalDateTime endDate) { if (startDate == null || endDate == null) { throw new CustomException(ErrorCode.BAD_REQUEST); @@ -558,6 +558,7 @@ private void validateDateTime(LocalDateTime startDate, LocalDateTime endDate) { throw new CustomException(ErrorCode.PLAN_INVALID_TIME_RANGE); } } + // private void validateRepeatRuleDate(StudyPlan studyPlan, LocalDate untilDate) { LocalDate planStartDate = studyPlan.getStartDate().toLocalDate(); diff --git a/src/main/java/com/back/domain/study/todo/controller/TodoController.java b/src/main/java/com/back/domain/study/todo/controller/TodoController.java new file mode 100644 index 00000000..c47b32f3 --- /dev/null +++ b/src/main/java/com/back/domain/study/todo/controller/TodoController.java @@ -0,0 +1,14 @@ +package com.back.domain.study.todo.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/todos") +@Tag(name = "Todo", description = "할 일 관련 API") +public class TodoController { + +} diff --git a/src/main/java/com/back/domain/study/todo/entity/Todo.java b/src/main/java/com/back/domain/study/todo/entity/Todo.java index 73c606a2..d1f082b2 100644 --- a/src/main/java/com/back/domain/study/todo/entity/Todo.java +++ b/src/main/java/com/back/domain/study/todo/entity/Todo.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -22,6 +23,6 @@ public class Todo extends BaseEntity { private String description; - private LocalDateTime date; + private LocalDate date; } diff --git a/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java b/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java new file mode 100644 index 00000000..e0192964 --- /dev/null +++ b/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java @@ -0,0 +1,9 @@ +package com.back.domain.study.todo.repository; + +import com.back.domain.study.todo.entity.Todo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TodoRepository extends JpaRepository { +} diff --git a/src/main/java/com/back/domain/study/todo/service/TodoService.java b/src/main/java/com/back/domain/study/todo/service/TodoService.java new file mode 100644 index 00000000..5937c956 --- /dev/null +++ b/src/main/java/com/back/domain/study/todo/service/TodoService.java @@ -0,0 +1,7 @@ +package com.back.domain.study.todo.service; + +import org.springframework.stereotype.Service; + +@Service +public class TodoService { +} From 61d8b1a9e09f2e97bf020e8d564e9c7c2780c70b Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Tue, 30 Sep 2025 10:44:55 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20plan=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B2=B9=EC=B9=A8=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/repository/StudyPlanRepository.java | 21 ++++++++ .../study/plan/service/StudyPlanService.java | 51 ++++++++++++++++++- .../com/back/global/exception/ErrorCode.java | 2 +- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/back/domain/study/plan/repository/StudyPlanRepository.java b/src/main/java/com/back/domain/study/plan/repository/StudyPlanRepository.java index 9c04caa9..00979e7a 100644 --- a/src/main/java/com/back/domain/study/plan/repository/StudyPlanRepository.java +++ b/src/main/java/com/back/domain/study/plan/repository/StudyPlanRepository.java @@ -2,11 +2,32 @@ import com.back.domain.study.plan.entity.StudyPlan; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.List; @Repository public interface StudyPlanRepository extends JpaRepository { List findByUserId(Long userId); + /* 시간 겹침 조건: + 새 계획 시작 시간보다 기존 계획 종료 시간이 늦고 (p.endDate > :newStart), + 새 계획 종료 시간보다 기존 계획 시작 시간이 빨라야 한다 (p.startDate < :newEnd). + (종료 시간 == 새 시작 시간)은 허용 + */ + @Query(""" + SELECT p + FROM StudyPlan p + WHERE p.user.id = :userId + AND (:planIdToExclude IS NULL OR p.id != :planIdToExclude) + AND p.endDate > :newStart + AND p.startDate < :newEnd +""") + List findByUserIdAndNotIdAndOverlapsTime( + Long userId, + Long planIdToExclude, + LocalDateTime newStart, + LocalDateTime newEnd + ); } diff --git a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java index 5faa87d5..e9cd077a 100644 --- a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java +++ b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java @@ -16,6 +16,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -42,6 +43,8 @@ public StudyPlanResponse createStudyPlan(Long userId, StudyPlanRequest request) // 날짜/시간 검증 validateDateTime(request.getStartDate(), request.getEndDate()); + // 시간 겹침 검증 + validateTimeConflict(userId, null, request.getStartDate(), request.getEndDate()); StudyPlan studyPlan = new StudyPlan(); @@ -311,6 +314,10 @@ public StudyPlanResponse updateStudyPlan(Long userId, Long planId, StudyPlanRequ .orElseThrow(() -> new CustomException(ErrorCode.PLAN_NOT_FOUND)); validateUserAccess(originalPlan, userId); + // 날짜/시간 검증 + validateDateTime(request.getStartDate(), request.getEndDate()); + // 시간 겹침 검증 (원본 계획 ID 제외) + validateTimeConflict(userId, originalPlan.getId(), request.getStartDate(), request.getEndDate()); // 1. 단발성 계획인 경우 if (originalPlan.getRepeatRule() == null) { @@ -558,7 +565,49 @@ private void validateDateTime(LocalDateTime startDate, LocalDateTime endDate) { throw new CustomException(ErrorCode.PLAN_INVALID_TIME_RANGE); } } - // + //시간 겹침 검증 메서드 (최적화된 DB 쿼리 + 가상 인스턴스 검증 조합) + private void validateTimeConflict(Long userId, Long planIdToExclude, LocalDateTime newStart, LocalDateTime newEnd) { + LocalDate newPlanDate = newStart.toLocalDate(); + + // 1. DB 쿼리를 통해 요청 시간과 원본 시간대가 겹칠 가능성이 있는 계획들만 로드 (최적화) + // 기존 조회 코드를 이용하려 했으나 성능 문제로 인해 쿼리 작성. + // 조회기능도 리펙토링 예정 + List conflictingOriginalPlans = studyPlanRepository.findByUserIdAndNotIdAndOverlapsTime( + userId, planIdToExclude, newStart, newEnd + ); + + if (conflictingOriginalPlans.isEmpty()) { + return; + } + + for (StudyPlan plan : conflictingOriginalPlans) { + if (plan.getRepeatRule() == null) { + // 2-1. 단발성 계획: 쿼리에서 이미 시간 범위가 겹친다고 걸러졌지만, 최종 확인 + if (isOverlapping(plan.getStartDate(), plan.getEndDate(), newStart, newEnd)) { + throw new CustomException(ErrorCode.PLAN_TIME_CONFLICT); + } + } else { + // 2-2. 반복 계획: 기존 헬퍼를 사용해 요청 날짜의 가상 인스턴스를 생성하고 검사 + StudyPlanResponse virtualPlan = createVirtualPlanForDate(plan, newPlanDate); + + if (virtualPlan != null) { + // 가상 인스턴스가 존재하고 (삭제되지 않았고) + // 해당 인스턴스의 확정된 시간이 새 계획과 겹치는지 최종 확인 + if (isOverlapping(virtualPlan.getStartDate(), virtualPlan.getEndDate(), newStart, newEnd)) { + throw new CustomException(ErrorCode.PLAN_TIME_CONFLICT); + } + } + } + } + } + /* + * 두 시간 범위의 겹침을 확인하는 메서드 + * 겹치는 조건: (새로운 시작 시각 < 기존 종료 시각) && (새로운 종료 시각 > 기존 시작 시각) + * (기존 종료 시각 == 새로운 시작 시각)은 겹치지 않는 것으로 간주 + */ + private boolean isOverlapping(LocalDateTime existingStart, LocalDateTime existingEnd, LocalDateTime newStart, LocalDateTime newEnd) { + return newStart.isBefore(existingEnd) && newEnd.isAfter(existingStart); + } private void validateRepeatRuleDate(StudyPlan studyPlan, LocalDate untilDate) { LocalDate planStartDate = studyPlan.getStartDate().toLocalDate(); diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index 612466bb..99873398 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -43,7 +43,7 @@ public enum ErrorCode { PLAN_ORIGINAL_REPEAT_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAN_004", "해당 날짜에 원본 반복 계획을 찾을 수 없습니다."), INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST, "PLAN_005", "날짜 형식이 올바르지 않습니다. (YYYY-MM-DD 형식을 사용해주세요)"), PLAN_INVALID_TIME_RANGE(HttpStatus.BAD_REQUEST, "PLAN_006", "시작 시간은 종료 시간보다 빨라야 합니다."), - PLAN_CONFLICT(HttpStatus.CONFLICT, "PLAN_007", "이미 존재하는 학습 계획과 시간이 겹칩니다."), + PLAN_TIME_CONFLICT(HttpStatus.CONFLICT, "PLAN_007", "이미 존재하는 학습 계획과 시간이 겹칩니다."), PLAN_CANNOT_UPDATE(HttpStatus.BAD_REQUEST, "PLAN_008", "수정 스위치 로직 탈출. 어떤 경우인지 파악이 필요합니다."), REPEAT_INVALID_UNTIL_DATE(HttpStatus.BAD_REQUEST, "REPEAT_001", "반복 계획의 종료 날짜는 시작 날짜 이전일 수 없습니다."), REPEAT_BYDAY_REQUIRED(HttpStatus.BAD_REQUEST, "REPEAT_002", "주간 반복 계획의 경우 요일(byDay) 정보가 필요합니다."), From 5cab4a976bc1304deae0ab45ac1d113273815fdb Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Tue, 30 Sep 2025 11:47:27 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20Todo=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/plan/service/StudyPlanService.java | 6 ++-- .../study/todo/controller/TodoController.java | 22 ++++++++++++ .../domain/study/todo/dto/TodoRequestDto.java | 15 ++++++++ .../study/todo/dto/TodoResponseDto.java | 23 ++++++++++++ .../back/domain/study/todo/entity/Todo.java | 9 ++++- .../study/todo/service/TodoService.java | 36 +++++++++++++++++++ .../com/back/global/exception/ErrorCode.java | 2 +- 7 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/back/domain/study/todo/dto/TodoRequestDto.java create mode 100644 src/main/java/com/back/domain/study/todo/dto/TodoResponseDto.java diff --git a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java index e9cd077a..3493c4d4 100644 --- a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java +++ b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java @@ -582,16 +582,16 @@ private void validateTimeConflict(Long userId, Long planIdToExclude, LocalDateTi for (StudyPlan plan : conflictingOriginalPlans) { if (plan.getRepeatRule() == null) { - // 2-1. 단발성 계획: 쿼리에서 이미 시간 범위가 겹친다고 걸러졌지만, 최종 확인 + // 2-1. 단발성 계획 -> 쿼리에서 이미 시간 범위가 겹친다고 걸러졌지만 재확인 if (isOverlapping(plan.getStartDate(), plan.getEndDate(), newStart, newEnd)) { throw new CustomException(ErrorCode.PLAN_TIME_CONFLICT); } } else { - // 2-2. 반복 계획: 기존 헬퍼를 사용해 요청 날짜의 가상 인스턴스를 생성하고 검사 + // 2-2. 반복 계획 -> 기존 메서드를 사용해 요청 날짜의 가상 인스턴스를 생성하고 검사 StudyPlanResponse virtualPlan = createVirtualPlanForDate(plan, newPlanDate); if (virtualPlan != null) { - // 가상 인스턴스가 존재하고 (삭제되지 않았고) + // 가상 인스턴스가 존재하고 // 해당 인스턴스의 확정된 시간이 새 계획과 겹치는지 최종 확인 if (isOverlapping(virtualPlan.getStartDate(), virtualPlan.getEndDate(), newStart, newEnd)) { throw new CustomException(ErrorCode.PLAN_TIME_CONFLICT); diff --git a/src/main/java/com/back/domain/study/todo/controller/TodoController.java b/src/main/java/com/back/domain/study/todo/controller/TodoController.java index c47b32f3..ad97d964 100644 --- a/src/main/java/com/back/domain/study/todo/controller/TodoController.java +++ b/src/main/java/com/back/domain/study/todo/controller/TodoController.java @@ -1,7 +1,18 @@ package com.back.domain.study.todo.controller; +import com.back.domain.study.todo.dto.TodoRequestDto; +import com.back.domain.study.todo.dto.TodoResponseDto; +import com.back.domain.study.todo.service.TodoService; +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.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -10,5 +21,16 @@ @RequestMapping("/api/todos") @Tag(name = "Todo", description = "할 일 관련 API") public class TodoController { + private final TodoService todoService; + + @PostMapping + @Operation(summary = "할 일 생성", description = "새로운 할 일을 생성합니다.") + public ResponseEntity> createTodo( + @AuthenticationPrincipal CustomUserDetails userDetails, + @Valid @RequestBody TodoRequestDto requestDto + ) { + TodoResponseDto response = todoService.createTodo(userDetails.getUserId(), requestDto); + return ResponseEntity.ok(RsData.success("할 일이 생성되었습니다.", response)); + } } diff --git a/src/main/java/com/back/domain/study/todo/dto/TodoRequestDto.java b/src/main/java/com/back/domain/study/todo/dto/TodoRequestDto.java new file mode 100644 index 00000000..1995084b --- /dev/null +++ b/src/main/java/com/back/domain/study/todo/dto/TodoRequestDto.java @@ -0,0 +1,15 @@ +package com.back.domain.study.todo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +public record TodoRequestDto( + @NotBlank(message = "할 일 설명은 필수입니다.") + String description, + @NotNull(message = "날짜는 필수입니다.") + LocalDate date +) { +} diff --git a/src/main/java/com/back/domain/study/todo/dto/TodoResponseDto.java b/src/main/java/com/back/domain/study/todo/dto/TodoResponseDto.java new file mode 100644 index 00000000..53a4c655 --- /dev/null +++ b/src/main/java/com/back/domain/study/todo/dto/TodoResponseDto.java @@ -0,0 +1,23 @@ +package com.back.domain.study.todo.dto; + +import com.back.domain.study.todo.entity.Todo; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; + +public record TodoResponseDto( + Long id, + String description, + boolean isComplete, + LocalDate date +) { + // entity -> DTO + public static TodoResponseDto from(Todo todo) { + return new TodoResponseDto( + todo.getId(), + todo.getDescription(), + todo.isComplete(), + todo.getDate() + ); + } +} diff --git a/src/main/java/com/back/domain/study/todo/entity/Todo.java b/src/main/java/com/back/domain/study/todo/entity/Todo.java index d1f082b2..806bb2df 100644 --- a/src/main/java/com/back/domain/study/todo/entity/Todo.java +++ b/src/main/java/com/back/domain/study/todo/entity/Todo.java @@ -15,7 +15,7 @@ @Getter @NoArgsConstructor public class Todo extends BaseEntity { - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "user_id") private User user; @@ -25,4 +25,11 @@ public class Todo extends BaseEntity { private LocalDate date; + public Todo(User user, String description, LocalDate date) { + this.user = user; + this.description = description; + this.date = date; + this.isComplete = false; + } + } diff --git a/src/main/java/com/back/domain/study/todo/service/TodoService.java b/src/main/java/com/back/domain/study/todo/service/TodoService.java index 5937c956..b13a7216 100644 --- a/src/main/java/com/back/domain/study/todo/service/TodoService.java +++ b/src/main/java/com/back/domain/study/todo/service/TodoService.java @@ -1,7 +1,43 @@ package com.back.domain.study.todo.service; +import com.back.domain.study.todo.dto.TodoRequestDto; +import com.back.domain.study.todo.dto.TodoResponseDto; +import com.back.domain.study.todo.entity.Todo; +import com.back.domain.study.todo.repository.TodoRepository; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.global.exception.CustomException; +import com.back.global.exception.ErrorCode; +import com.back.global.security.user.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor +@Transactional(readOnly = true) public class TodoService { + private final TodoRepository todoRepository; + private final UserRepository userRepository; + + // ==================== 생성 =================== + public TodoResponseDto createTodo(Long userId, TodoRequestDto request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + Todo todo = new Todo( + user, + request.description(), + request.date() + ); + + Todo savedTodo = todoRepository.save(todo); + return TodoResponseDto.from(savedTodo); + } + + // ==================== 조회 =================== + + // ==================== 수정 =================== + + // ==================== 삭제 =================== } diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index 99873398..9366b88d 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -43,7 +43,7 @@ public enum ErrorCode { PLAN_ORIGINAL_REPEAT_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAN_004", "해당 날짜에 원본 반복 계획을 찾을 수 없습니다."), INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST, "PLAN_005", "날짜 형식이 올바르지 않습니다. (YYYY-MM-DD 형식을 사용해주세요)"), PLAN_INVALID_TIME_RANGE(HttpStatus.BAD_REQUEST, "PLAN_006", "시작 시간은 종료 시간보다 빨라야 합니다."), - PLAN_TIME_CONFLICT(HttpStatus.CONFLICT, "PLAN_007", "이미 존재하는 학습 계획과 시간이 겹칩니다."), + PLAN_TIME_CONFLICT(HttpStatus.CONFLICT, "PLAN_007", "이미 존재하는 학습 계획과 시간이 겹칩니다. 기존 종료 시간과 겹치는 경우는 제외됩니다."), PLAN_CANNOT_UPDATE(HttpStatus.BAD_REQUEST, "PLAN_008", "수정 스위치 로직 탈출. 어떤 경우인지 파악이 필요합니다."), REPEAT_INVALID_UNTIL_DATE(HttpStatus.BAD_REQUEST, "REPEAT_001", "반복 계획의 종료 날짜는 시작 날짜 이전일 수 없습니다."), REPEAT_BYDAY_REQUIRED(HttpStatus.BAD_REQUEST, "REPEAT_002", "주간 반복 계획의 경우 요일(byDay) 정보가 필요합니다."), From b4890ef626798106c6fdeab340bea1bf9842c65e Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Tue, 30 Sep 2025 12:13:40 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/todo/controller/TodoController.java | 38 +++++++++++++++++-- .../study/todo/repository/TodoRepository.java | 6 +++ .../study/todo/service/TodoService.java | 19 +++++++++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/back/domain/study/todo/controller/TodoController.java b/src/main/java/com/back/domain/study/todo/controller/TodoController.java index ad97d964..5418acf0 100644 --- a/src/main/java/com/back/domain/study/todo/controller/TodoController.java +++ b/src/main/java/com/back/domain/study/todo/controller/TodoController.java @@ -9,12 +9,13 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; @RestController @RequiredArgsConstructor @@ -23,6 +24,7 @@ public class TodoController { private final TodoService todoService; + // ==================== 생성 =================== @PostMapping @Operation(summary = "할 일 생성", description = "새로운 할 일을 생성합니다.") public ResponseEntity> createTodo( @@ -33,4 +35,32 @@ public ResponseEntity> createTodo( return ResponseEntity.ok(RsData.success("할 일이 생성되었습니다.", response)); } + // ==================== 조회 =================== + // 특정 날짜 조회 + @GetMapping + @Operation(summary = "할 일 목록 조회", description = "조건에 따라 할 일 목록을 조회합니다. " + + "date만 제공시 해당 날짜, startDate와 endDate 제공시 기간별, 아무것도 없으면 전체 조회") + public ResponseEntity>> getTodos( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date + ) { + List response = todoService.getTodosByDate(userDetails.getUserId(), date); + + return ResponseEntity.ok(RsData.success("할 일 목록을 조회했습니다.", response)); + } + + // 사용자의 모든 할 일 조회 + @GetMapping("/all") + @Operation(summary = "모든 할 일 조회", description = "사용자의 모든 할 일을 조회합니다.") + public ResponseEntity>> getAllTodos( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + List response = todoService.getAllTodos(userDetails.getUserId()); + return ResponseEntity.ok(RsData.success("모든 할 일을 조회했습니다.", response)); + } + + // ==================== 수정 =================== + + // ==================== 삭제 =================== + } diff --git a/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java b/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java index e0192964..89bf755f 100644 --- a/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java +++ b/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java @@ -1,9 +1,15 @@ package com.back.domain.study.todo.repository; import com.back.domain.study.todo.entity.Todo; +import com.back.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.time.LocalDate; +import java.util.List; + @Repository public interface TodoRepository extends JpaRepository { + List findByUserIdAndDate(Long userId, LocalDate date); + List findByUserId(Long userId); } diff --git a/src/main/java/com/back/domain/study/todo/service/TodoService.java b/src/main/java/com/back/domain/study/todo/service/TodoService.java index b13a7216..d38d750b 100644 --- a/src/main/java/com/back/domain/study/todo/service/TodoService.java +++ b/src/main/java/com/back/domain/study/todo/service/TodoService.java @@ -14,6 +14,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -36,7 +40,20 @@ public TodoResponseDto createTodo(Long userId, TodoRequestDto request) { } // ==================== 조회 =================== - + // 유저의 특정 날짜의 모든 할 일 조회 + public List getTodosByDate(Long userId, LocalDate date) { + List todos = todoRepository.findByUserIdAndDate(userId, date); + return todos.stream() + .map(TodoResponseDto::from) + .collect(Collectors.toList()); + } + // 유저의 전체 할 일 조회 + public List getAllTodos(Long userId) { + List todos = todoRepository.findByUserId(userId); + return todos.stream() + .map(TodoResponseDto::from) + .collect(Collectors.toList()); + } // ==================== 수정 =================== // ==================== 삭제 =================== From 9ef56b2fb30ff8a7f4db9211ad118869bb4a631a Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Tue, 30 Sep 2025 12:58:56 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat,refact=20:=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?+=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/todo/controller/TodoController.java | 32 ++++++++++ .../back/domain/study/todo/entity/Todo.java | 7 +++ .../study/todo/repository/TodoRepository.java | 2 + .../study/todo/service/TodoService.java | 60 ++++++++++++++++--- .../com/back/global/exception/ErrorCode.java | 4 ++ 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/back/domain/study/todo/controller/TodoController.java b/src/main/java/com/back/domain/study/todo/controller/TodoController.java index 5418acf0..e0eea722 100644 --- a/src/main/java/com/back/domain/study/todo/controller/TodoController.java +++ b/src/main/java/com/back/domain/study/todo/controller/TodoController.java @@ -60,7 +60,39 @@ public ResponseEntity>> getAllTodos( } // ==================== 수정 =================== + // 할 일 내용 수정 + @PutMapping("/{todoId}") + @Operation(summary = "할 일 수정", description = "할 일의 내용과 날짜를 수정합니다.") + public ResponseEntity> updateTodo( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long todoId, + @Valid @RequestBody TodoRequestDto requestDto + ) { + TodoResponseDto response = todoService.updateTodo(userDetails.getUserId(), todoId, requestDto); + return ResponseEntity.ok(RsData.success("할 일이 수정되었습니다.", response)); + } + + // 할 일 완료/미완료 토글 + @PutMapping("/{todoId}/complete") + @Operation(summary = "할 일 완료 상태 토글", description = "할 일의 완료 상태를 변경합니다.") + public ResponseEntity> toggleTodoComplete( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long todoId + ) { + TodoResponseDto response = todoService.toggleTodoComplete(userDetails.getUserId(), todoId); + return ResponseEntity.ok(RsData.success("할 일 상태가 변경되었습니다.", response)); + } // ==================== 삭제 =================== + // 할 일 삭제 + @DeleteMapping("/{todoId}") + @Operation(summary = "할 일 삭제", description = "할 일을 삭제합니다.") + public ResponseEntity> deleteTodo( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long todoId + ) { + todoService.deleteTodo(userDetails.getUserId(), todoId); + return ResponseEntity.ok(RsData.success("할 일이 삭제되었습니다.")); + } } diff --git a/src/main/java/com/back/domain/study/todo/entity/Todo.java b/src/main/java/com/back/domain/study/todo/entity/Todo.java index 806bb2df..23d22e35 100644 --- a/src/main/java/com/back/domain/study/todo/entity/Todo.java +++ b/src/main/java/com/back/domain/study/todo/entity/Todo.java @@ -32,4 +32,11 @@ public Todo(User user, String description, LocalDate date) { this.isComplete = false; } + public void updateDescription(String description) { + this.description = description; + } + + public void toggleComplete() { + this.isComplete = !this.isComplete; + } } diff --git a/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java b/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java index 89bf755f..aede5ce4 100644 --- a/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java +++ b/src/main/java/com/back/domain/study/todo/repository/TodoRepository.java @@ -7,9 +7,11 @@ import java.time.LocalDate; import java.util.List; +import java.util.Optional; @Repository public interface TodoRepository extends JpaRepository { List findByUserIdAndDate(Long userId, LocalDate date); List findByUserId(Long userId); + Todo findByIdAndUser(Long id, User user); } diff --git a/src/main/java/com/back/domain/study/todo/service/TodoService.java b/src/main/java/com/back/domain/study/todo/service/TodoService.java index d38d750b..d1bb891d 100644 --- a/src/main/java/com/back/domain/study/todo/service/TodoService.java +++ b/src/main/java/com/back/domain/study/todo/service/TodoService.java @@ -8,8 +8,6 @@ import com.back.domain.user.repository.UserRepository; import com.back.global.exception.CustomException; import com.back.global.exception.ErrorCode; -import com.back.global.security.user.CustomUserDetails; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,9 +24,10 @@ public class TodoService { private final UserRepository userRepository; // ==================== 생성 =================== + @Transactional public TodoResponseDto createTodo(Long userId, TodoRequestDto request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + User user = findUserById(userId); + Todo todo = new Todo( user, request.description(), @@ -40,21 +39,68 @@ public TodoResponseDto createTodo(Long userId, TodoRequestDto request) { } // ==================== 조회 =================== - // 유저의 특정 날짜의 모든 할 일 조회 + //유저의 특정 날짜의 모든 할 일 조회 public List getTodosByDate(Long userId, LocalDate date) { List todos = todoRepository.findByUserIdAndDate(userId, date); return todos.stream() .map(TodoResponseDto::from) .collect(Collectors.toList()); } - // 유저의 전체 할 일 조회 + + //유저의 전체 할 일 조회 public List getAllTodos(Long userId) { List todos = todoRepository.findByUserId(userId); return todos.stream() .map(TodoResponseDto::from) .collect(Collectors.toList()); } + // ==================== 수정 =================== + // 할 일 내용 수정 + @Transactional + public TodoResponseDto updateTodo(Long userId, Long todoId, TodoRequestDto requestDto) { + User user = findUserById(userId); + Todo todo = findTodoByIdAndUser(todoId, user); + + todo.updateDescription(requestDto.description()); + + return TodoResponseDto.from(todo); + } + + // 할 일 완료 상태 토글 + @Transactional + public TodoResponseDto toggleTodoComplete(Long userId, Long todoId) { + User user = findUserById(userId); + Todo todo = findTodoByIdAndUser(todoId, user); + todo.toggleComplete(); + return TodoResponseDto.from(todo); + } // ==================== 삭제 =================== -} + // 할 일 삭제 + @Transactional + public void deleteTodo(Long userId, Long todoId) { + User user = findUserById(userId); + Todo todo = findTodoByIdAndUser(todoId, user); + todoRepository.delete(todo); + } + + // ==================== 유틸 =================== + // 유저 조회 + private User findUserById(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } + + // 할 일 조회 및 소유권 확인 + private Todo findTodoByIdAndUser(Long todoId, User user) { + Todo todo = todoRepository.findById(todoId) + .orElseThrow(() -> new CustomException(ErrorCode.TODO_NOT_FOUND)); + + if (!todo.getUser().getId().equals(user.getId())) { + throw new CustomException(ErrorCode.TODO_FORBIDDEN); + } + + return todo; + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index 9366b88d..3b25db35 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -48,6 +48,10 @@ public enum ErrorCode { REPEAT_INVALID_UNTIL_DATE(HttpStatus.BAD_REQUEST, "REPEAT_001", "반복 계획의 종료 날짜는 시작 날짜 이전일 수 없습니다."), REPEAT_BYDAY_REQUIRED(HttpStatus.BAD_REQUEST, "REPEAT_002", "주간 반복 계획의 경우 요일(byDay) 정보가 필요합니다."), + // ======================== 투두 관련 ======================== + TODO_NOT_FOUND(HttpStatus.NOT_FOUND, "TODO_001", "존재하지 않는 할 일입니다."), + TODO_FORBIDDEN(HttpStatus.FORBIDDEN, "TODO_002", "할 일에 대한 접근 권한이 없습니다."), + // ======================== 메시지 관련 ======================== MESSAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "MESSAGE_001", "존재하지 않는 메시지입니다."), From ddf1d315c1fb27e42712f2a91ece5422f1861055 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Tue, 30 Sep 2025 15:30:17 +0900 Subject: [PATCH 6/6] unknown --- .../java/com/back/domain/study/todo/service/TodoService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/back/domain/study/todo/service/TodoService.java b/src/main/java/com/back/domain/study/todo/service/TodoService.java index d1bb891d..d2783757 100644 --- a/src/main/java/com/back/domain/study/todo/service/TodoService.java +++ b/src/main/java/com/back/domain/study/todo/service/TodoService.java @@ -92,7 +92,7 @@ private User findUserById(Long userId) { .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); } - // 할 일 조회 및 소유권 확인 + // 할 일 조회 및 사용자 소유 검증 private Todo findTodoByIdAndUser(Long todoId, User user) { Todo todo = todoRepository.findById(todoId) .orElseThrow(() -> new CustomException(ErrorCode.TODO_NOT_FOUND));