Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ boolean existsByTodo_IdAndTargetDateAndStatusIn(
@Query("delete from TimerRecord r where r.todo.id = :todoId")
void deleteByTodoId(@Param("todoId") Long todoId);

@Query("""
select count(r) > 0
from TimerRecord r
where r.todo.id = :todoId
and r.status in :statuses
and r.targetDate >= :fromDate
""")
boolean existsByTodoIdAndTargetDateFromAndStatusIn(
@Param("todoId") Long todoId,
@Param("fromDate") LocalDate fromDate,
@Param("statuses") List<TimerStatus> statuses
);

@Modifying(clearAutomatically = true)
@Query("delete from TimerRecord r where r.todo.id = :todoId and r.targetDate >= :fromDate")
void deleteByTodoIdAndTargetDateFrom(
@Param("todoId") Long todoId,
@Param("fromDate") LocalDate fromDate
);

@Modifying(flushAutomatically = true)
@Query("""
update TimerRecord r
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.Timo.Timo.domain.timer.repository;

import com.Timo.Timo.domain.timer.entity.TimerSession;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
Expand All @@ -19,6 +20,17 @@ public interface TimerSessionRepository extends JpaRepository<TimerSession, Long
@Query("delete from TimerSession s where s.timerRecord.todo.id = :todoId")
void deleteByTodoId(@Param("todoId") Long todoId);

@Modifying(clearAutomatically = true)
@Query("""
delete from TimerSession s
where s.timerRecord.todo.id = :todoId
and s.timerRecord.targetDate >= :fromDate
""")
void deleteByTodoIdAndTargetDateFrom(
@Param("todoId") Long todoId,
@Param("fromDate") LocalDate fromDate
);

@Query("""
select ts
from TimerSession ts
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/Timo/Timo/domain/timer/service/TimerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ public void deleteTimersByTodo(Long todoId) {
timerRecordRepository.deleteByTodoId(todoId);
}

public boolean hasActiveTimerFrom(Long todoId, LocalDate fromDate) {
return timerRecordRepository.existsByTodoIdAndTargetDateFromAndStatusIn(todoId, fromDate, ACTIVE_STATUS);
}

@Transactional
public void deleteTimersByTodoFrom(Long todoId, LocalDate fromDate) {
timerSessionRepository.deleteByTodoIdAndTargetDateFrom(todoId, fromDate);
timerRecordRepository.deleteByTodoIdAndTargetDateFrom(todoId, fromDate);
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public TimerFinishResponse completeTimer(Long userId, Long timerId) {
return finishTimer(userId, timerId, TimerStatus.COMPLETED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,10 @@ public ResponseEntity<BaseResponse<Object>> updateTodo(
@DeleteMapping("/{todoId}")
public ResponseEntity<BaseResponse<Object>> deleteTodo(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long todoId
@PathVariable Long todoId,
@RequestParam(required = false) String date
) {
todoService.deleteTodo(userDetails.getUserId(), todoId);
todoService.deleteTodo(userDetails.getUserId(), todoId, date);

return ResponseEntity
.status(TodoSuccessCode.DELETED.getHttpStatus())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ ResponseEntity<BaseResponse<Object>> updateTodo(
TODO와 연결된 하위 태스크, 반복 규칙을 함께 삭제합니다.
해당 TODO에 실행 중이거나 일시정지된 타이머가 있으면 삭제할 수 없습니다.

date에는 삭제를 누른 카드의 날짜를 반복 여부와 상관없이 항상 전달합니다.
반복 TODO는 date를 포함한 이후 반복 일정만 삭제하고, date 이전 일정과 완료·타이머 기록은 유지합니다.
이때 반복 종료일은 date 전날로 변경되며, date 이후 일정에 실행 중이거나 일시정지된 타이머가 있으면 삭제할 수 없습니다.
반복이 아닌 TODO는 해당 TODO 전체를 삭제합니다.
기존 클라이언트 호환을 위해 당분간 date 없이 요청하면 반복 여부와 상관없이 TODO 전체를 삭제합니다.

구글 캘린더에서 가져온 일정은 삭제할 수 없습니다.
단, 구글에서 삭제되었지만 타이머 기록이 있어 남아 있는 일정(isDeletedFromGoogle=true)과
캘린더 연동 해제 후 남은 일정(isDisconnectedFromGoogle=true)은 삭제할 수 있습니다.
Expand All @@ -376,6 +382,14 @@ ResponseEntity<BaseResponse<Object>> updateTodo(
description = "TODO 삭제 성공",
useReturnTypeSchema = true
),
@ApiResponse(
responseCode = "400",
description = "date의 날짜 형식(yyyy-MM-dd)이 올바르지 않은 경우",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorDto.class)
)
),
@ApiResponse(
responseCode = "401",
description = "Access Token이 없거나 만료되었거나 유효하지 않은 경우",
Expand Down Expand Up @@ -411,7 +425,8 @@ ResponseEntity<BaseResponse<Object>> updateTodo(
})
ResponseEntity<BaseResponse<Object>> deleteTodo(
@Parameter(hidden = true) CustomUserDetails userDetails,
@Parameter(description = "삭제할 TODO ID", example = "205") Long todoId
@Parameter(description = "삭제할 TODO ID", example = "205") Long todoId,
@Parameter(description = "삭제를 누른 카드의 날짜 (yyyy-MM-dd)", example = "2026-10-20", required = true) String date
);

@Operation(
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/Timo/Timo/domain/todo/entity/Todo.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ public void changeSchedule(
this.repeatDayOfMonth = repeatType == RepeatType.MONTHLY ? repeatDayOfMonth : null;
}

public boolean isRepeating() {
return repeatType != RepeatType.NONE;
}

public void endRepeatBefore(LocalDate date) {
if (!date.isAfter(endDate)) {
this.endDate = date.minusDays(1);
}
}

public void replaceSubtasks(List<SubtaskEdit> edits) {
Map<Long, Subtask> existingById = subtasks.stream()
.filter(subtask -> subtask.getId() != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.Timo.Timo.domain.todo.repository;

import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand All @@ -22,6 +23,11 @@ public interface SubtaskCompletionRepository extends JpaRepository<SubtaskComple
+ "(select ti.id from TodoInstance ti where ti.todo.id = :todoId)")
void deleteByTodoId(@Param("todoId") Long todoId);

@Modifying(clearAutomatically = true)
@Query("delete from SubtaskCompletion sc where sc.todoInstance.id in "
+ "(select ti.id from TodoInstance ti where ti.todo.id = :todoId and ti.date >= :fromDate)")
void deleteByTodoIdAndDateFrom(@Param("todoId") Long todoId, @Param("fromDate") LocalDate fromDate);

@Modifying(clearAutomatically = true)
@Query("delete from SubtaskCompletion sc where sc.subtask.id in :subtaskIds")
void deleteBySubtaskIdIn(@Param("subtaskIds") Collection<Long> subtaskIds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public interface TodoInstanceRepository extends JpaRepository<TodoInstance, Long
@Query("delete from TodoInstance i where i.todo.id = :todoId")
void deleteByTodoId(@Param("todoId") Long todoId);

@Modifying(clearAutomatically = true)
@Query("delete from TodoInstance i where i.todo.id = :todoId and i.date >= :fromDate")
void deleteByTodoIdAndDateFrom(@Param("todoId") Long todoId, @Param("fromDate") LocalDate fromDate);

@Query("""
select ti
from TodoInstance ti
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ public boolean occursOn(Todo rule, LocalDate date) {
};
}

public boolean occursBefore(Todo rule, LocalDate date) {
LocalDate end = date.isAfter(rule.getEndDate()) ? rule.getEndDate().plusDays(1) : date;
if (!end.isAfter(rule.getStartDate())) {
return false;
}

return rule.getStartDate().datesUntil(end)
.anyMatch(candidate -> occursOn(rule, candidate));
}

private boolean matchesWeekly(LocalDate date, List<Weekday> weekdays) {
Set<DayOfWeek> targets = weekdays.stream()
.map(DAY_OF_WEEKS::get)
Expand Down
32 changes: 29 additions & 3 deletions src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,42 @@ public void updateTodo(Long userId, Long todoId, TodoUpdateRequest request) {
}

@Transactional
public void deleteTodo(Long userId, Long todoId) {
public void deleteTodo(Long userId, Long todoId, String dateValue) {
LocalDate date = dateValue == null ? null : parseDate(dateValue);
userRepository.findByIdForUpdate(userId)
.orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));
Todo todo = todoRepository.findByIdAndUser_Id(todoId, userId)
.orElseThrow(() -> new CustomException(TodoErrorCode.TODO_NOT_FOUND));

if (todo.isGoogleEvent() && !todo.isDeletedFromGoogle() && !todo.isDisconnectedFromGoogle()) {
throw new CustomException(TodoErrorCode.GOOGLE_EVENT_NOT_DELETABLE);
}

if (date != null && todo.isRepeating() && todoDateCalculator.occursBefore(todo, date)) {
removeTodoFrom(todo, date);
} else {
removeTodo(todo);
}

aiHistoryCacheService.bumpUserHistoryVersionAfterCommit(userId);
}

private void removeTodoFrom(Todo todo, LocalDate date) {
Long todoId = todo.getId();
if (timerService.hasActiveTimerFrom(todoId, date)) {
throw new CustomException(TodoErrorCode.TIMER_RUNNING);
}

todo.endRepeatBefore(date);
todoRepository.flush();

timerService.deleteTimersByTodoFrom(todoId, date);
subtaskCompletionRepository.deleteByTodoIdAndDateFrom(todoId, date);
todoInstanceRepository.deleteByTodoIdAndDateFrom(todoId, date);
}

private void removeTodo(Todo todo) {
Long todoId = todo.getId();
if (timerService.hasActiveTimer(todoId)) {
throw new CustomException(TodoErrorCode.TIMER_RUNNING);
}
Expand All @@ -273,8 +301,6 @@ public void deleteTodo(Long userId, Long todoId) {
subtaskCompletionRepository.deleteByTodoId(todoId);
todoInstanceRepository.deleteByTodoId(todoId);
todoRepository.delete(todo);

aiHistoryCacheService.bumpUserHistoryVersionAfterCommit(userId);
}

private void validateGoogleEventUpdatable(Todo todo, TodoUpdateRequest request) {
Expand Down
Loading