Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 14 additions & 4 deletions src/main/java/com/umc/timeto/todo/controller/TodoController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import com.umc.timeto.global.apiPayload.code.ResponseCode;
import com.umc.timeto.global.apiPayload.dto.ResponseDTO;
import com.umc.timeto.todo.dto.request.TodoCreateRequest;
import com.umc.timeto.todo.dto.request.TodoOrderUpdateRequest;
import com.umc.timeto.todo.dto.request.TodoStatusUpdateRequest;
import com.umc.timeto.todo.dto.request.TodoUpdateRequest;
import com.umc.timeto.todo.dto.response.TodoCreateResponse;
import com.umc.timeto.todo.dto.response.TodoGetResponse;
import com.umc.timeto.todo.dto.response.TodoIngListResponse;
import com.umc.timeto.todo.dto.response.TodoStatusUpdateResponse;
import com.umc.timeto.todo.dto.response.*;
import com.umc.timeto.todo.service.TodoCommandService;
import com.umc.timeto.todo.service.TodoQueryService;
import com.umc.timeto.todo.service.TodoService;
Expand Down Expand Up @@ -135,4 +133,16 @@ public ResponseEntity<ResponseDTO<List<TodoGetResponse>>> getUnblockedTodos(
);
}

@Operation(summary = "할 일 순서 변경", description = "todoId를 targetOrder로 이동시킵니다. (state별 정렬)")
@PatchMapping("/order/{todoId}")
public ResponseDTO<TodoOrderUpdateResponse> updateTodoOrder(
@PathVariable Long todoId,
@RequestBody @Valid TodoOrderUpdateRequest request
) {
Long memberId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
TodoOrderUpdateResponse data = todoCommandService.updateTodoOrder(memberId, todoId, request.getTargetOrder());
return new ResponseDTO<>(ResponseCode.COMMON200, data);
}


}
17 changes: 15 additions & 2 deletions src/main/java/com/umc/timeto/todo/domain/Todo.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ public class Todo {
@Column(name = "start_at")
private LocalDateTime startAt;

// 추가
@Column(name = "sort_order", nullable = false)
private Integer sortOrder;

// getter는 @Getter로 자동

public void changeSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}


// ✅ 지금은 연관관계 없이 숫자 FK만 들고감
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "folder_id", nullable = false)
Expand All @@ -60,17 +71,19 @@ public class Todo {



public static Todo create(Folder folder, String name, TodoPriority priority, LocalTime duration) {
public static Todo create(Folder folder, String name, TodoPriority priority, LocalTime duration, int sortOrder) {
Todo todo = new Todo();
todo.folder = folder;
todo.name = name;
todo.priority = priority;
todo.duration = duration;
todo.state = TodoState.progress; // 기본값
todo.state = TodoState.progress;
todo.createAt = LocalDateTime.now();
todo.sortOrder = sortOrder;
return todo;
}


public void changeState(TodoState state) {
this.state = state;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.umc.timeto.todo.dto.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class TodoOrderUpdateRequest {
@NotNull
@Min(1)
private Integer targetOrder;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ public class TodoGetResponse {
private TodoPriority priority;
private TodoState state;
private LocalDateTime startAt;
private Integer sortOrder;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ public static class TodoIngItem {
private TodoPriority priority;
private String duration; // "1H 10M" 형태
private LocalDateTime startAt;
private Integer sortOrder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.umc.timeto.todo.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TodoOrderUpdateResponse {
private Long todoId;
private Integer sortOrder;
}
110 changes: 110 additions & 0 deletions src/main/java/com/umc/timeto/todo/repository/TodoRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,116 @@ public interface TodoRepository extends JpaRepository<Todo, Long> {
List<Todo> findAllByFolder_FolderIdAndFolder_Goal_Member_MemberIdAndState(
Long folderId, Long memberId, TodoState state
);
List<Todo> findAllByFolder_FolderIdAndFolder_Goal_Member_MemberIdAndStateOrderBySortOrderAsc(
Long folderId, Long memberId, TodoState state
);

@Query("""
select coalesce(max(t.sortOrder), 0)
from Todo t
where t.folder.folderId = :folderId
and t.state = :state
and exists (
select 1
from Folder f
join f.goal g
where f = t.folder
and g.member.memberId = :memberId
)
""")
int findMaxSortOrder(@Param("folderId") Long folderId,
@Param("memberId") Long memberId,
@Param("state") TodoState state);


@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update Todo t
set t.sortOrder = t.sortOrder - 1
where t.folder.folderId = :folderId
and t.state = :state
and t.sortOrder > :deletedOrder
and exists (
select 1
from Folder f
join f.goal g
where f = t.folder
and g.member.memberId = :memberId
)
""")
void pullUpAfterDelete(@Param("folderId") Long folderId,
@Param("memberId") Long memberId,
@Param("state") TodoState state,
@Param("deletedOrder") int deletedOrder);


@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update Todo t
set t.sortOrder = t.sortOrder + 1
where t.folder.folderId = :folderId
and t.state = :state
and t.sortOrder >= :targetOrder
and t.sortOrder < :currentOrder
and exists (
select 1
from Folder f
join f.goal g
where f = t.folder
and g.member.memberId = :memberId
)
""")
void shiftDownForMoveUp(@Param("folderId") Long folderId,
@Param("memberId") Long memberId,
@Param("state") TodoState state,
@Param("targetOrder") int targetOrder,
@Param("currentOrder") int currentOrder);


@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update Todo t
set t.sortOrder = t.sortOrder - 1
where t.folder.folderId = :folderId
and t.state = :state
and t.sortOrder > :currentOrder
and t.sortOrder <= :targetOrder
and exists (
select 1
from Folder f
join f.goal g
where f = t.folder
and g.member.memberId = :memberId
)
""")
void shiftUpForMoveDown(@Param("folderId") Long folderId,
@Param("memberId") Long memberId,
@Param("state") TodoState state,
@Param("currentOrder") int currentOrder,
@Param("targetOrder") int targetOrder);


@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update Todo t
set t.sortOrder = t.sortOrder - 1
where t.folder.folderId = :folderId
and t.state = :fromState
and t.sortOrder > :fromOrder
and exists (
select 1
from Folder f
join f.goal g
where f = t.folder
and g.member.memberId = :memberId
)
""")
void pullUpAfterStateMove(@Param("folderId") Long folderId,
@Param("memberId") Long memberId,
@Param("fromState") TodoState fromState,
@Param("fromOrder") int fromOrder);



// ✅ 내 todo만 삭제
void deleteByTodoIdAndFolder_Goal_Member_MemberId(Long todoId, Long memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.umc.timeto.todo.dto.request.TodoStatusUpdateRequest;
import com.umc.timeto.todo.dto.request.TodoUpdateRequest;
import com.umc.timeto.todo.dto.response.TodoGetResponse;
import com.umc.timeto.todo.dto.response.TodoOrderUpdateResponse;
import com.umc.timeto.todo.dto.response.TodoStatusUpdateResponse;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -15,5 +16,8 @@ public interface TodoCommandService {

@Transactional(readOnly = true)
List<TodoGetResponse> getUnblockedTodos(Long memberId,Long folderId);

TodoOrderUpdateResponse updateTodoOrder(Long memberId, Long todoId, int targetOrder);

}

101 changes: 85 additions & 16 deletions src/main/java/com/umc/timeto/todo/service/TodoCommandServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import com.umc.timeto.global.apiPayload.code.ErrorCode;
import com.umc.timeto.global.apiPayload.exception.GlobalException;
import com.umc.timeto.todo.domain.Todo;
import com.umc.timeto.todo.domain.enums.TodoState;
import com.umc.timeto.todo.dto.request.TodoStatusUpdateRequest;
import com.umc.timeto.todo.dto.request.TodoUpdateRequest;
import com.umc.timeto.todo.dto.response.TodoGetResponse;
import com.umc.timeto.todo.dto.response.TodoOrderUpdateResponse;
import com.umc.timeto.todo.dto.response.TodoStatusUpdateResponse;
import com.umc.timeto.todo.repository.TodoRepository;
import com.umc.timeto.todo.util.DurationFormatter;
Expand All @@ -25,18 +27,46 @@ public class TodoCommandServiceImpl implements TodoCommandService{
private final BlockService blockService;

@Override
public TodoStatusUpdateResponse updateStatus(Long memberId,Long todoId, TodoStatusUpdateRequest request) {
public TodoStatusUpdateResponse updateStatus(Long memberId, Long todoId, TodoStatusUpdateRequest request) {

Todo todo = todoRepository.findByTodoIdAndFolder_Goal_Member_MemberId(todoId,memberId)
.orElseThrow(() -> new GlobalException(ErrorCode.TODO_NOT_FOUND)); // 너희 ErrorCode로 교체
Todo todo = todoRepository.findByTodoIdAndFolder_Goal_Member_MemberId(todoId, memberId)
.orElseThrow(() -> new GlobalException(ErrorCode.TODO_NOT_FOUND));

todo.changeState(request.getState());
TodoState fromState = todo.getState();
TodoState toState = request.getState();

return new TodoStatusUpdateResponse(
todo.getTodoId(),
todo.getState().name() // "COMPLETE"
);
if (fromState != toState) {

Long folderId = todo.getFolder().getFolderId();

// 1️⃣ 상태 먼저 변경
todo.changeState(toState);

// 2️⃣ 기존 state 정렬 재정립
List<Todo> fromList =
todoRepository.findAllByFolder_FolderIdAndFolder_Goal_Member_MemberIdAndStateOrderBySortOrderAsc(
folderId, memberId, fromState
);

int order = 1;
for (Todo t : fromList) {
t.changeSortOrder(order++);
}

// 3️⃣ 새 state에서 맨 아래 배치
List<Todo> toList =
todoRepository.findAllByFolder_FolderIdAndFolder_Goal_Member_MemberIdAndStateOrderBySortOrderAsc(
folderId, memberId, toState
);

todo.changeSortOrder(toList.size());
}

return new TodoStatusUpdateResponse(todo.getTodoId(), todo.getState().name());
}



@Override
public TodoGetResponse updateTodo(Long memberId, Long todoId, TodoUpdateRequest request) {

Expand All @@ -63,20 +93,25 @@ public TodoGetResponse updateTodo(Long memberId, Long todoId, TodoUpdateRequest
DurationFormatter.format(todo.getDuration()),
todo.getPriority(),
todo.getState(),
todo.getStartAt()
todo.getStartAt(),
todo.getSortOrder()
);
}
@Override
@Transactional
public void deleteTodo(Long memberId, Long todoId) {
boolean exists = todoRepository.existsByTodoIdAndFolder_Goal_Member_MemberId(todoId, memberId);
if (!exists) {
throw new GlobalException(ErrorCode.TODO_NOT_FOUND);
}
todoRepository.deleteByTodoIdAndFolder_Goal_Member_MemberId(todoId, memberId);
Todo todo = todoRepository.findByTodoIdAndFolder_Goal_Member_MemberId(todoId, memberId)
.orElseThrow(() -> new GlobalException(ErrorCode.TODO_NOT_FOUND));

Long folderId = todo.getFolder().getFolderId();
TodoState state = todo.getState();
int deletedOrder = todo.getSortOrder();

todoRepository.delete(todo); // 또는 기존 deleteBy...
todoRepository.pullUpAfterDelete(folderId, memberId, state, deletedOrder);
}



@Transactional(readOnly = true)
@Override
public List<TodoGetResponse> getUnblockedTodos(Long memberId,Long folderId) {
Expand All @@ -95,9 +130,43 @@ public List<TodoGetResponse> getUnblockedTodos(Long memberId,Long folderId) {
DurationFormatter.format(todo.getDuration()),
todo.getPriority(),
todo.getState(),
todo.getStartAt()
todo.getStartAt(),
todo.getSortOrder()
))
.toList();
}

@Transactional
public TodoOrderUpdateResponse updateTodoOrder(Long memberId, Long todoId, int targetOrder) {
Todo todo = todoRepository.findByTodoIdAndFolder_Goal_Member_MemberId(todoId, memberId)
.orElseThrow(() -> new GlobalException(ErrorCode.TODO_NOT_FOUND));

Long folderId = todo.getFolder().getFolderId();
TodoState state = todo.getState();

int currentOrder = todo.getSortOrder();

int max = todoRepository.findMaxSortOrder(folderId, memberId, state);
int clamped = Math.max(1, Math.min(targetOrder, max));

if (clamped == currentOrder) {
return new TodoOrderUpdateResponse(todoId, currentOrder);
}

if (clamped < currentOrder) {
todoRepository.shiftDownForMoveUp(folderId, memberId, state, clamped, currentOrder);
} else {
todoRepository.shiftUpForMoveDown(folderId, memberId, state, currentOrder, clamped);
}

// ✅ bulk update 이후에는 다시 조회해서 안전하게 반영
Todo refreshed = todoRepository.findById(todoId)
.orElseThrow(() -> new GlobalException(ErrorCode.TODO_NOT_FOUND));
refreshed.changeSortOrder(clamped);

return new TodoOrderUpdateResponse(todoId, clamped);
}



}
Loading