diff --git a/src/main/java/com/umc/timeto/block/controller/BlockController.java b/src/main/java/com/umc/timeto/block/controller/BlockController.java new file mode 100644 index 0000000..3a192a8 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/controller/BlockController.java @@ -0,0 +1,125 @@ +package com.umc.timeto.block.controller; + +import com.umc.timeto.block.dto.BlockAddDTO; +import com.umc.timeto.block.dto.BlockResponseNumDTO; +import com.umc.timeto.block.service.BlockService; +import com.umc.timeto.global.apiPayload.code.ResponseCode; +import com.umc.timeto.global.apiPayload.dto.ResponseDTO; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/block") +public class BlockController { + + private final BlockService blockService; + + private Long getMemberId(Authentication authentication) { + return (Long) authentication.getPrincipal(); + } + + @Operation(summary = "타임블럭 저장", description = "할 일 시작 시간을 받아서 블록을 저장합니다. 블록에는 일정 겹침 검사가 존재합니다." + + "(ex: 일정 1이 7:30~8:30 일떄, 일정2의 생성/이동은 8:30분 이상부터 가능)") + @PatchMapping("/{todoId}") + public ResponseEntity createBlock( + @PathVariable Long todoId, + @RequestBody BlockAddDTO req, + Authentication authentication + ) { + var res= blockService.createBlock(todoId, req, getMemberId(authentication)); + + return ResponseEntity + .status(ResponseCode.SUCCESS_ADD_BLOCK.getStatus().value()) + .body(new ResponseDTO<>(ResponseCode.SUCCESS_ADD_BLOCK, res)); + } + + @Operation(summary = "날짜별 타임블럭 조회", description = "입력받은 날짜(yyyy-MM-DD)에 생성된 타임 블럭들을 조회합니다 블록 조회 기본(메인) 화면에 사용합니다. ") + @GetMapping("/day") + public ResponseEntity getBlockByDay( + // 기본 format: yyyy-MM-DD + @RequestParam LocalDate date, + Authentication authentication + ) { + var res= blockService.getBlockByDay(date, getMemberId(authentication)); + + return ResponseEntity + .status(ResponseCode.SUCCESS_GET_BLOCKLIST.getStatus().value()) + .body(new ResponseDTO<>(ResponseCode.SUCCESS_GET_BLOCKLIST, res)); + } + + @Operation(summary = "한달 날짜별 타임블럭 수", + description = "입력받은 날짜(YYYY-MM)에 생성된 타임 블럭 수를 조회합니다") + @GetMapping("/month") + public ResponseEntity>> getBlockNumByMonth( + @RequestParam + @DateTimeFormat(pattern = "yyyy-MM") + YearMonth yearMonth, + Authentication authentication + ) { + + var res = blockService.getBlockNumByMonth(yearMonth, getMemberId(authentication)); + + return ResponseEntity + .status(ResponseCode.SUCCESS_GET_BLOCK_NUMBER.getStatus().value()) + .body(new ResponseDTO<>(ResponseCode.SUCCESS_GET_BLOCK_NUMBER, res)); + } + + @Operation(summary = "블록 소요시간 변경", + description = "블록 소요시간 변경 시 사용합니다. 할 일 내부에서만 소요시간 변경이 가능하다면 사용하지 않아도 됩니다. ") + @PatchMapping("/{blockId}/duration") + public ResponseEntity> updateDuration( + @PathVariable Long blockId, + @RequestParam LocalTime duration, + Authentication authentication + ) { + + blockService.updateBlockDuration( + blockId, + getMemberId(authentication), + duration + ); + + return ResponseEntity.ok( + new ResponseDTO<>(ResponseCode.SUCCESS_UPDATE_BLOCK, null) + ); + } + + + @Operation(summary = "블록 이동", + description = "블록을 드래그&드롭으로 이동했을 때 정보를 갱신합니다. 변경된 시작 시간을 입력으로 받습니다. " + + "이동된 시간이 다른 일정과 겹칠 경우 갱신되지 않습니다. startAt format: yyyy-MM-dd'T'HH:mm") + @PatchMapping("/{blockId}/move") + public ResponseEntity> moveBlock( + @PathVariable Long blockId, + @RequestParam + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + LocalDateTime startAt, + Authentication authentication + ) { + + blockService.moveBlock( + blockId, + getMemberId(authentication), + startAt + ); + + return ResponseEntity.ok( + new ResponseDTO<>(ResponseCode.SUCCESS_UPDATE_BLOCK, null) + ); + } + + + + +} diff --git a/src/main/java/com/umc/timeto/block/dto/BlockAddDTO.java b/src/main/java/com/umc/timeto/block/dto/BlockAddDTO.java new file mode 100644 index 0000000..ee90040 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/dto/BlockAddDTO.java @@ -0,0 +1,21 @@ +package com.umc.timeto.block.dto; + + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BlockAddDTO { + @NotBlank(message = "startAt은 필수 입력 값입니다.") + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + private LocalDateTime startAt; +} diff --git a/src/main/java/com/umc/timeto/block/dto/BlockResponseDTO.java b/src/main/java/com/umc/timeto/block/dto/BlockResponseDTO.java new file mode 100644 index 0000000..e2ed89b --- /dev/null +++ b/src/main/java/com/umc/timeto/block/dto/BlockResponseDTO.java @@ -0,0 +1,18 @@ +package com.umc.timeto.block.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BlockResponseDTO { + private Long blockId; + private Long todoId; + private LocalDateTime startAt; + private LocalDateTime endAt; +} diff --git a/src/main/java/com/umc/timeto/block/dto/BlockResponseDetailDTO.java b/src/main/java/com/umc/timeto/block/dto/BlockResponseDetailDTO.java new file mode 100644 index 0000000..0c77669 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/dto/BlockResponseDetailDTO.java @@ -0,0 +1,28 @@ +package com.umc.timeto.block.dto; + +import com.umc.timeto.todo.domain.enums.TodoPriority; +import com.umc.timeto.todo.domain.enums.TodoState; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BlockResponseDetailDTO { + private Long blockId; + private Long todoId; + private LocalDateTime startAt; + private LocalDateTime endAt; + private String todoName; + private TodoPriority priority; + private TodoState state; + private String goalName; + private String color; + + +} diff --git a/src/main/java/com/umc/timeto/block/dto/BlockResponseNumDTO.java b/src/main/java/com/umc/timeto/block/dto/BlockResponseNumDTO.java new file mode 100644 index 0000000..85e6384 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/dto/BlockResponseNumDTO.java @@ -0,0 +1,18 @@ +package com.umc.timeto.block.dto; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BlockResponseNumDTO { + private LocalDate date; + private Long count; +} diff --git a/src/main/java/com/umc/timeto/block/entity/Block.java b/src/main/java/com/umc/timeto/block/entity/Block.java new file mode 100644 index 0000000..9c27dc9 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/entity/Block.java @@ -0,0 +1,68 @@ +package com.umc.timeto.block.entity; + +import com.umc.timeto.todo.domain.Todo; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Entity +@Getter +@NoArgsConstructor +public class Block { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long blockId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "todo_id") + private Todo todo; + + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + private LocalDateTime startAt; + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + private LocalDateTime endAt; + // block에서는 startAt, endAt 입력받고 todoo 에 startAt 저장 및 duration update + //Duration: localtime + + public Block(Todo todo, LocalDateTime startAt) { + this.todo=todo; + + LocalDateTime normalizedStart = normalize(startAt); + this.startAt = normalizedStart; + LocalTime duration = todo.getDuration(); + + LocalDateTime calculatedEnd = normalizedStart + .plusHours(duration.getHour()) + .plusMinutes(duration.getMinute()); + + this.endAt = normalize(calculatedEnd); + } + + public void updateTime(LocalDateTime startAt, LocalDateTime endAt) { + this.startAt = normalize(startAt); + this.endAt = normalize(endAt); + } + + private LocalDateTime normalize(LocalDateTime time) { + return time.withSecond(0).withNano(0); + } + + @PrePersist + @PreUpdate + private void trimSeconds() { + if (startAt != null) { + startAt = startAt.withSecond(0).withNano(0); + } + if (endAt != null) { + endAt = endAt.withSecond(0).withNano(0); + } + } + + + +} \ No newline at end of file diff --git a/src/main/java/com/umc/timeto/block/repository/BlockRepository.java b/src/main/java/com/umc/timeto/block/repository/BlockRepository.java new file mode 100644 index 0000000..b5e3fa9 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/repository/BlockRepository.java @@ -0,0 +1,40 @@ +package com.umc.timeto.block.repository; + +import com.umc.timeto.block.entity.Block; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface BlockRepository extends JpaRepository { + Optional findByTodo_TodoId(Long todoId); + + + List findByTodo_Folder_Goal_Member_MemberIdAndStartAtBetween( + Long memberId, + LocalDateTime start, + LocalDateTime end + ); + List findByTodo_Folder_Goal_Member_MemberIdAndStartAtGreaterThanEqualAndStartAtLessThan( + Long memberId, + LocalDateTime start, + LocalDateTime end + ); + + + List findByTodo_Folder_Goal_Member_MemberIdAndStartAtLessThanAndEndAtGreaterThan( + Long memberId, + LocalDateTime endAt, + LocalDateTime startAt + ); + + List findByTodo_Folder_Goal_Member_MemberIdAndBlockIdNotAndStartAtLessThanAndEndAtGreaterThan( + Long memberId, + Long blockId, + LocalDateTime endAt, + LocalDateTime startAt + ); + + Optional findByBlockIdAndTodo_Folder_Goal_Member_MemberId(Long blockId, Long memberId); +} diff --git a/src/main/java/com/umc/timeto/block/service/BlockService.java b/src/main/java/com/umc/timeto/block/service/BlockService.java new file mode 100644 index 0000000..3715d0a --- /dev/null +++ b/src/main/java/com/umc/timeto/block/service/BlockService.java @@ -0,0 +1,33 @@ +package com.umc.timeto.block.service; + +import com.umc.timeto.block.dto.BlockAddDTO; +import com.umc.timeto.block.dto.BlockResponseDTO; +import com.umc.timeto.block.dto.BlockResponseDetailDTO; +import com.umc.timeto.block.dto.BlockResponseNumDTO; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.List; + +public interface BlockService { + BlockResponseDTO createBlock(Long todoId, BlockAddDTO req, Long memberId); + + + List getBlockByDay(LocalDate date, Long memberId); + + List getBlockNumByMonth(YearMonth yearMonth, Long memberId); + + BlockResponseDTO updateBlockDuration(Long blockId, Long memberId, LocalTime newDuration); + void updateBlockDurationByTodo(Long todoId, Long memberId, LocalTime newDuration); + + + BlockResponseDTO moveBlock(Long blockId, Long memberId, LocalDateTime newStart); + + + + + + +} diff --git a/src/main/java/com/umc/timeto/block/service/BlockServiceImpl.java b/src/main/java/com/umc/timeto/block/service/BlockServiceImpl.java new file mode 100644 index 0000000..35d5bc1 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/service/BlockServiceImpl.java @@ -0,0 +1,274 @@ +package com.umc.timeto.block.service; + +import com.umc.timeto.block.dto.BlockAddDTO; +import com.umc.timeto.block.dto.BlockResponseDTO; +import com.umc.timeto.block.dto.BlockResponseDetailDTO; +import com.umc.timeto.block.dto.BlockResponseNumDTO; +import com.umc.timeto.block.entity.Block; +import com.umc.timeto.block.repository.BlockRepository; +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.repository.TodoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional +public class BlockServiceImpl implements BlockService { + + private final TodoRepository todoRepository; + private final BlockRepository blockRepository; + + @Override + public BlockResponseDTO createBlock(Long todoId, BlockAddDTO req, Long memberId) { + + Todo todo = todoRepository.findByTodoIdAndFolder_Goal_Member_MemberId(todoId,memberId) + .orElseThrow(() -> new GlobalException(ErrorCode.TODO_NOT_FOUND)); + + + // 블록 겹침 조회 + LocalDateTime startAt = req.getStartAt(); + LocalTime duration = todo.getDuration(); + + LocalDateTime endAt = startAt + .plusHours(duration.getHour()) + .plusMinutes(duration.getMinute()) + .plusSeconds(duration.getSecond()); + System.out.println("시작시간" +startAt + "끝나는시간" +endAt); + + List overlaps = + blockRepository + .findByTodo_Folder_Goal_Member_MemberIdAndStartAtLessThanAndEndAtGreaterThan( + memberId, + endAt, + startAt + ); + + if (!overlaps.isEmpty()) { + throw new GlobalException(ErrorCode.BLOCK_TIME_CONFLICT); + } + + + //블록 저장 + Block block = new Block(todo, startAt); + Block savedBlock = blockRepository.save(block); + + + return BlockResponseDTO.builder() + .blockId(savedBlock.getBlockId()) + .todoId(todoId) + .startAt(savedBlock.getStartAt()) + .endAt(savedBlock.getEndAt()) + .build(); + } + + @Override + public List getBlockByDay(LocalDate date, Long memberId) { + + LocalDateTime start = date.atStartOfDay(); + LocalDateTime end = date.atTime(23, 59, 59); + + List blocks = + blockRepository + .findByTodo_Folder_Goal_Member_MemberIdAndStartAtBetween( + memberId, + start, + end + ); + + + return blocks.stream() + .map(block -> BlockResponseDetailDTO.builder() + .blockId(block.getBlockId()) + .todoId(block.getTodo().getTodoId()) + .startAt(block.getStartAt()) + .endAt(block.getEndAt()) + .todoName(block.getTodo().getName()) + .priority(block.getTodo().getPriority()) + .state(block.getTodo().getState()) + .goalName(block.getTodo().getFolder().getGoal().getName()) + .color(block.getTodo().getFolder().getGoal().getColor()) + .build()) + .collect(Collectors.toList()); + } + + @Override + public List getBlockNumByMonth(YearMonth yearMonth, Long memberId) { + + LocalDateTime start = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime nextMonthStart = yearMonth.plusMonths(1) + .atDay(1) + .atStartOfDay(); + + List blocks = + blockRepository.findByTodo_Folder_Goal_Member_MemberIdAndStartAtGreaterThanEqualAndStartAtLessThan( + memberId, + start, + nextMonthStart + ); + + return blocks.stream() + .collect(Collectors.groupingBy( + block -> block.getStartAt().toLocalDate(), + Collectors.counting() + )) + .entrySet() + .stream() + .map(entry -> new BlockResponseNumDTO( + entry.getKey(), + entry.getValue() + )) + .toList(); + } + + @Override + public BlockResponseDTO updateBlockDuration(Long blockId, Long memberId, LocalTime newDuration) { + + Block block = blockRepository + .findByBlockIdAndTodo_Folder_Goal_Member_MemberId(blockId, memberId) + .orElseThrow(() -> new GlobalException(ErrorCode.BLOCK_NOT_FOUND)); + + LocalDateTime newStart = block.getStartAt(); + LocalDateTime newEnd = newStart + .plusHours(newDuration.getHour()) + .plusMinutes(newDuration.getMinute()) + .plusSeconds(newDuration.getSecond()); + + List overlaps = + blockRepository + .findByTodo_Folder_Goal_Member_MemberIdAndBlockIdNotAndStartAtLessThanAndEndAtGreaterThan( + memberId, + blockId, + newEnd, + newStart + ); + + if (!overlaps.isEmpty()) { + throw new GlobalException(ErrorCode.BLOCK_TIME_CONFLICT); + } + + block.updateTime(newStart, newEnd); + + // Todo 동기화 + Todo todo = block.getTodo(); + todo.changeDuration(newDuration); + todo.updateStartAt(newStart); + + + return BlockResponseDTO.builder() + .blockId(block.getBlockId()) + .todoId(todo.getTodoId()) + .startAt(block.getStartAt()) + .endAt(block.getEndAt()) + .build(); + } + + + @Override + public void updateBlockDurationByTodo(Long todoId, + Long memberId, + LocalTime newDuration) { + + Todo todo = todoRepository + .findByTodoIdAndFolder_Goal_Member_MemberId(todoId, memberId) + .orElseThrow(() -> new GlobalException(ErrorCode.TODO_NOT_FOUND)); + + Optional optionalBlock = + blockRepository.findByTodo_TodoId(todoId); + + // Block이 존재하는 경우 + if (optionalBlock.isPresent()) { + + Block block = optionalBlock.get(); + + LocalDateTime newStart = block.getStartAt(); + LocalDateTime newEnd = newStart + .plusHours(newDuration.getHour()) + .plusMinutes(newDuration.getMinute()) + .plusSeconds(newDuration.getSecond()); + + // 충돌 검사 + List overlaps = + blockRepository + .findByTodo_Folder_Goal_Member_MemberIdAndBlockIdNotAndStartAtLessThanAndEndAtGreaterThan( + memberId, + block.getBlockId(), + newEnd, + newStart + ); + + if (!overlaps.isEmpty()) { + throw new GlobalException(ErrorCode.BLOCK_TIME_CONFLICT); + } + + block.updateTime(newStart, newEnd); + todo.changeDuration(newDuration); + todo.updateStartAt(newStart); + + } + // Block이 없는 경우 + else { + todo.changeDuration(newDuration); + } + } + + + + + @Override + public BlockResponseDTO moveBlock(Long blockId, Long memberId, LocalDateTime newStart) { + + Block block = blockRepository + .findByBlockIdAndTodo_Folder_Goal_Member_MemberId(blockId, memberId) + .orElseThrow(() -> new GlobalException(ErrorCode.BLOCK_NOT_FOUND)); + + LocalTime duration = block.getTodo().getDuration(); + + LocalDateTime newEnd = newStart + .plusHours(duration.getHour()) + .plusMinutes(duration.getMinute()) + .plusSeconds(duration.getSecond()); + + List overlaps = + blockRepository + .findByTodo_Folder_Goal_Member_MemberIdAndBlockIdNotAndStartAtLessThanAndEndAtGreaterThan( + memberId, + blockId, + newEnd, + newStart + ); + + if (!overlaps.isEmpty()) { + throw new GlobalException(ErrorCode.BLOCK_TIME_CONFLICT); + } + + block.updateTime(newStart, newEnd); + + // Todo 동기화 + Todo todo = block.getTodo(); + todo.updateStartAt(newStart); + long seconds = java.time.Duration.between(newStart, newEnd).getSeconds(); + LocalTime newDuration = LocalTime.ofSecondOfDay(seconds); + todo.changeDuration(newDuration); + + return BlockResponseDTO.builder() + .blockId(block.getBlockId()) + .todoId(todo.getTodoId()) + .startAt(block.getStartAt()) + .endAt(block.getEndAt()) + .build(); + + } + +} diff --git a/src/main/java/com/umc/timeto/global/apiPayload/code/ErrorCode.java b/src/main/java/com/umc/timeto/global/apiPayload/code/ErrorCode.java index 5154b8f..6245a3e 100644 --- a/src/main/java/com/umc/timeto/global/apiPayload/code/ErrorCode.java +++ b/src/main/java/com/umc/timeto/global/apiPayload/code/ErrorCode.java @@ -12,6 +12,8 @@ public enum ErrorCode { * 400 BAD_REQUEST - 잘못된 요청 */ BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + BLOCK_TIME_CONFLICT(HttpStatus.BAD_REQUEST, "이미 해당 시간에 블록이 존재합니다."), + /** * 401 UNAUTHORIZED - 인증 실패 @@ -32,6 +34,7 @@ public enum ErrorCode { GOAL_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 아이디를 가진 목표가 존재하지 않습니다."), FOLDER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 아이디를 가진 폴더가 존재하지 않습니다."), TODO_NOT_FOUND(HttpStatus.NOT_FOUND,"해당 아이디를 가진 할 일이 존재하지 않습니다."), + BLOCK_NOT_FOUND(HttpStatus.NOT_FOUND,"해당 아이디를 가진 블록이 존재하지 않습니다."), LOG_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 아이디를 가진 일지가 존재하지 않습니다."), diff --git a/src/main/java/com/umc/timeto/global/apiPayload/code/ResponseCode.java b/src/main/java/com/umc/timeto/global/apiPayload/code/ResponseCode.java index d20d72d..23cbae7 100644 --- a/src/main/java/com/umc/timeto/global/apiPayload/code/ResponseCode.java +++ b/src/main/java/com/umc/timeto/global/apiPayload/code/ResponseCode.java @@ -24,6 +24,17 @@ public enum ResponseCode { SUCCESS_UPDATE_FOLDER(HttpStatus.OK, "폴더를 성공적으로 수정했습니다."), SUCCESS_DELETE_FOLDER(HttpStatus.OK, "폴더를 성공적으로 삭제했습니다."), + + /** + * block + */ + SUCCESS_ADD_BLOCK(HttpStatus.CREATED, "블록을 성공적으로 등록했습니다."), + SUCCESS_GET_BLOCKLIST(HttpStatus.OK, "블록 리스트를 성공적으로 불러왔습니다."), + SUCCESS_GET_BLOCK_NUMBER(HttpStatus.OK, "블록 수를 성공적으로 불러왔습니다."), + SUCCESS_UPDATE_BLOCK(HttpStatus.OK, "블록을 성공적으로 업데이트했습니다"), + // 블록 생성 시 할일 조회 + SUCCESS_GET_UNBLOCKED_TODOS(HttpStatus.OK, "블록을 생성할 할 일을 성공적으로 불러왔습니다."), + // Common COMMON200(HttpStatus.OK, "요청에 성공하였습니다."), COMMON201(HttpStatus.CREATED, "회원 가입 및 로그인 성공"), diff --git a/src/main/java/com/umc/timeto/todo/controller/TodoController.java b/src/main/java/com/umc/timeto/todo/controller/TodoController.java index 6b5f522..c45fefb 100644 --- a/src/main/java/com/umc/timeto/todo/controller/TodoController.java +++ b/src/main/java/com/umc/timeto/todo/controller/TodoController.java @@ -15,8 +15,12 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import org.springframework.security.core.Authentication; + +import java.util.List; @RestController @RequiredArgsConstructor @@ -28,6 +32,10 @@ public class TodoController { private final TodoQueryService todoQueryService; + private Long getMemberId(Authentication authentication) { + return (Long) authentication.getPrincipal(); + } + @Operation(summary = "할 일 정보 조회", description = "todoId로 할 일 상세 정보를 조회합니다.") @GetMapping("/{todoId}") public ResponseDTO getTodo(@PathVariable Long todoId) { @@ -109,4 +117,22 @@ public ResponseDTO deleteTodo(@PathVariable Long todoId) { todoCommandService.deleteTodo(memberId, todoId); return new ResponseDTO<>(ResponseCode.COMMON200, "성공"); } + + @Operation(summary = "블록 생성 후보 할일 조회", description = "블록이 등록되지 않은 할 일을 조회합니다") + @GetMapping("/{folderId}/todo/unblocked") + public ResponseEntity>> getUnblockedTodos( + @PathVariable Long folderId, + Authentication authentication + ) { + + Long memberId = getMemberId(authentication); + + return ResponseEntity.ok( + new ResponseDTO<>( + ResponseCode.SUCCESS_GET_UNBLOCKED_TODOS, + todoCommandService.getUnblockedTodos(memberId,folderId) + ) + ); + } + } diff --git a/src/main/java/com/umc/timeto/todo/domain/Todo.java b/src/main/java/com/umc/timeto/todo/domain/Todo.java index 36665d0..df2ca19 100644 --- a/src/main/java/com/umc/timeto/todo/domain/Todo.java +++ b/src/main/java/com/umc/timeto/todo/domain/Todo.java @@ -1,6 +1,7 @@ package com.umc.timeto.todo.domain; +import com.umc.timeto.block.entity.Block; import com.umc.timeto.folder.entity.Folder; import com.umc.timeto.todo.domain.enums.TodoPriority; import com.umc.timeto.todo.domain.enums.TodoState; @@ -49,6 +50,16 @@ public class Todo { private Folder folder; + //todo 삭제 시 연결된 block 자동 삭제 + @OneToOne( + mappedBy = "todo", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private Block block; + + + public static Todo create(Folder folder, String name, TodoPriority priority, LocalTime duration) { Todo todo = new Todo(); todo.folder = folder; @@ -68,4 +79,6 @@ public void changeState(TodoState state) { public void changePriority(TodoPriority priority) { this.priority = priority; } public void changeDuration(LocalTime duration) { this.duration = duration; } + public void updateStartAt(LocalDateTime startAt) { this.startAt = startAt;} + } diff --git a/src/main/java/com/umc/timeto/todo/repository/TodoRepository.java b/src/main/java/com/umc/timeto/todo/repository/TodoRepository.java index 1423587..626dfbe 100644 --- a/src/main/java/com/umc/timeto/todo/repository/TodoRepository.java +++ b/src/main/java/com/umc/timeto/todo/repository/TodoRepository.java @@ -23,6 +23,13 @@ List findAllByFolder_FolderIdAndFolder_Goal_Member_MemberIdAndState( boolean existsByTodoIdAndFolder_Goal_Member_MemberId(Long todoId, Long memberId); + // block 후보 조회에 사용 + List findByFolder_FolderIdAndFolder_Goal_Member_MemberIdAndBlockIsNull( + Long folderId, + Long memberId + ); + + @Query(""" select t.folder.folderId as folderId, count(t) as cnt from Todo t diff --git a/src/main/java/com/umc/timeto/todo/service/TodoCommandService.java b/src/main/java/com/umc/timeto/todo/service/TodoCommandService.java index 6fff9fd..ae3dcea 100644 --- a/src/main/java/com/umc/timeto/todo/service/TodoCommandService.java +++ b/src/main/java/com/umc/timeto/todo/service/TodoCommandService.java @@ -4,10 +4,16 @@ import com.umc.timeto.todo.dto.request.TodoUpdateRequest; import com.umc.timeto.todo.dto.response.TodoGetResponse; import com.umc.timeto.todo.dto.response.TodoStatusUpdateResponse; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; public interface TodoCommandService { TodoStatusUpdateResponse updateStatus(Long memberId, Long todoId, TodoStatusUpdateRequest request); TodoGetResponse updateTodo(Long memberId, Long todoId, TodoUpdateRequest request); void deleteTodo(Long memberId, Long todoId); + + @Transactional(readOnly = true) + List getUnblockedTodos(Long memberId,Long folderId); } diff --git a/src/main/java/com/umc/timeto/todo/service/TodoCommandServiceImpl.java b/src/main/java/com/umc/timeto/todo/service/TodoCommandServiceImpl.java index 6774c08..63fdef8 100644 --- a/src/main/java/com/umc/timeto/todo/service/TodoCommandServiceImpl.java +++ b/src/main/java/com/umc/timeto/todo/service/TodoCommandServiceImpl.java @@ -1,6 +1,5 @@ package com.umc.timeto.todo.service; - -import com.umc.timeto.folder.repository.FolderRepository; +import com.umc.timeto.block.service.BlockService; import com.umc.timeto.global.apiPayload.code.ErrorCode; import com.umc.timeto.global.apiPayload.exception.GlobalException; import com.umc.timeto.todo.domain.Todo; @@ -16,13 +15,14 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalTime; +import java.util.List; @Service @RequiredArgsConstructor @Transactional public class TodoCommandServiceImpl implements TodoCommandService{ private final TodoRepository todoRepository; - private final FolderRepository folderRepository; + private final BlockService blockService; @Override public TodoStatusUpdateResponse updateStatus(Long memberId,Long todoId, TodoStatusUpdateRequest request) { @@ -51,10 +51,10 @@ public TodoGetResponse updateTodo(Long memberId, Long todoId, TodoUpdateRequest if (request.getPriority() != null) { todo.changePriority(request.getPriority()); } - + // duration 변경 시 Block이 처리 if (request.getDuration() != null && !request.getDuration().isBlank()) { LocalTime parsed = DurationParser.parseToLocalTime(request.getDuration()); - todo.changeDuration(parsed); + blockService.updateBlockDurationByTodo(todoId, memberId, parsed); } return new TodoGetResponse( @@ -75,4 +75,29 @@ public void deleteTodo(Long memberId, Long todoId) { } todoRepository.deleteByTodoIdAndFolder_Goal_Member_MemberId(todoId, memberId); } + + + @Transactional(readOnly = true) + @Override + public List getUnblockedTodos(Long memberId,Long folderId) { + + List todos = + todoRepository + .findByFolder_FolderIdAndFolder_Goal_Member_MemberIdAndBlockIsNull( + folderId, + memberId + ); + + return todos.stream() + .map(todo -> new TodoGetResponse( + todo.getTodoId(), + todo.getName(), + DurationFormatter.format(todo.getDuration()), + todo.getPriority(), + todo.getState(), + todo.getStartAt() + )) + .toList(); + } + } diff --git a/src/main/java/com/umc/timeto/todo/service/TodoQueryServiceImpl.java b/src/main/java/com/umc/timeto/todo/service/TodoQueryServiceImpl.java index 45093e4..b7b49cd 100644 --- a/src/main/java/com/umc/timeto/todo/service/TodoQueryServiceImpl.java +++ b/src/main/java/com/umc/timeto/todo/service/TodoQueryServiceImpl.java @@ -1,6 +1,4 @@ package com.umc.timeto.todo.service; - -import com.umc.timeto.folder.repository.FolderRepository; import com.umc.timeto.global.apiPayload.code.ErrorCode; import com.umc.timeto.global.apiPayload.exception.GlobalException; import com.umc.timeto.todo.domain.Todo;