diff --git a/src/main/java/com/Timo/Timo/domain/focus/controller/FocusController.java b/src/main/java/com/Timo/Timo/domain/focus/controller/FocusController.java new file mode 100644 index 0000000..d3b5ecd --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/focus/controller/FocusController.java @@ -0,0 +1,38 @@ +package com.Timo.Timo.domain.focus.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.Timo.Timo.domain.focus.docs.FocusControllerDocs; +import com.Timo.Timo.domain.focus.dto.FocusTodoResult; +import com.Timo.Timo.domain.focus.dto.response.FocusTodoResponse; +import com.Timo.Timo.domain.focus.service.FocusService; +import com.Timo.Timo.global.auth.principal.CustomUserDetails; +import com.Timo.Timo.global.response.BaseResponse; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/v1/focus") +@RequiredArgsConstructor +@Tag(name = "Focus", description = "집중 모드 API") +public class FocusController implements FocusControllerDocs { + + private final FocusService focusService; + + @Override + @GetMapping + public ResponseEntity> getFocusTodo( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + FocusTodoResult result = focusService.getFocusTodo(userDetails.getUserId()); + + return ResponseEntity + .status(result.successCode().getHttpStatus()) + .body(BaseResponse.onSuccess(result.successCode(), result.response())); + } +} diff --git a/src/main/java/com/Timo/Timo/domain/focus/docs/FocusControllerDocs.java b/src/main/java/com/Timo/Timo/domain/focus/docs/FocusControllerDocs.java new file mode 100644 index 0000000..1d7ac75 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/focus/docs/FocusControllerDocs.java @@ -0,0 +1,62 @@ +package com.Timo.Timo.domain.focus.docs; + +import org.springframework.http.ResponseEntity; + +import com.Timo.Timo.domain.focus.dto.response.FocusTodoResponse; +import com.Timo.Timo.global.auth.principal.CustomUserDetails; +import com.Timo.Timo.global.exception.dto.ErrorDto; +import com.Timo.Timo.global.response.BaseResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +public interface FocusControllerDocs { + + @Operation( + summary = "집중 모드 TODO 조회", + description = """ + 오늘 TODO 중 미완료인 최상단 TODO 하나를 조회합니다. (sortOrder가 가장 작은 미완료 TODO) + 오늘 TODO가 하나도 없으면 "오늘의 TODO가 없습니다.", + 오늘 TODO를 모두 완료했으면 "오늘의 TODO를 모두 완료했습니다." 메시지를 반환하며, + 두 경우 모두 hasTodo는 false, todo는 null입니다. + """ + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "집중 모드 TODO 조회 성공 / 오늘의 TODO가 없거나 모두 완료한 경우", + useReturnTypeSchema = true + ), + @ApiResponse( + responseCode = "401", + description = "Access Token이 없거나 만료되었거나 유효하지 않은 경우", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDto.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 사용자인 경우", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDto.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDto.class) + ) + ) + }) + ResponseEntity> getFocusTodo( + @Parameter(hidden = true) CustomUserDetails userDetails + ); +} diff --git a/src/main/java/com/Timo/Timo/domain/focus/dto/FocusTodoResult.java b/src/main/java/com/Timo/Timo/domain/focus/dto/FocusTodoResult.java new file mode 100644 index 0000000..69f1713 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/focus/dto/FocusTodoResult.java @@ -0,0 +1,9 @@ +package com.Timo.Timo.domain.focus.dto; + +import com.Timo.Timo.domain.focus.dto.response.FocusTodoResponse; +import com.Timo.Timo.domain.focus.exception.FocusSuccessCode; + +public record FocusTodoResult( + FocusSuccessCode successCode, + FocusTodoResponse response +) { } diff --git a/src/main/java/com/Timo/Timo/domain/focus/dto/response/FocusTodoResponse.java b/src/main/java/com/Timo/Timo/domain/focus/dto/response/FocusTodoResponse.java new file mode 100644 index 0000000..e89457f --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/focus/dto/response/FocusTodoResponse.java @@ -0,0 +1,62 @@ +package com.Timo.Timo.domain.focus.dto.response; + +import java.time.LocalDate; +import java.util.List; + +import com.Timo.Timo.domain.home.dto.response.HomeResponse.SubtaskResponse; +import com.Timo.Timo.domain.home.dto.response.HomeResponse.TagResponse; +import com.Timo.Timo.domain.home.dto.response.HomeResponse.TodoResponse; +import com.Timo.Timo.domain.todo.enums.Weekday; +import com.fasterxml.jackson.annotation.JsonProperty; + +public record FocusTodoResponse( + LocalDate date, + Weekday dayOfWeek, + boolean hasTodo, + FocusTodoDetailResponse todo +) { + public static FocusTodoResponse of(LocalDate date, TodoResponse todo) { + return new FocusTodoResponse( + date, + Weekday.from(date.getDayOfWeek()), + true, + FocusTodoDetailResponse.from(todo) + ); + } + + public static FocusTodoResponse empty(LocalDate date) { + return new FocusTodoResponse( + date, + Weekday.from(date.getDayOfWeek()), + false, + null + ); + } + + public record FocusTodoDetailResponse( + Long todoId, + String icon, + String title, + boolean completed, + Integer durationSeconds, + String priority, + TagResponse tag, + @JsonProperty("isRepeated") + boolean isRepeated, + List subtasks + ) { + public static FocusTodoDetailResponse from(TodoResponse todo) { + return new FocusTodoDetailResponse( + todo.todoId(), + todo.icon(), + todo.title(), + todo.completed(), + todo.durationSeconds(), + todo.priority(), + todo.tag(), + todo.isRepeated(), + todo.subtasks() + ); + } + } +} diff --git a/src/main/java/com/Timo/Timo/domain/focus/exception/FocusSuccessCode.java b/src/main/java/com/Timo/Timo/domain/focus/exception/FocusSuccessCode.java new file mode 100644 index 0000000..eb487a0 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/focus/exception/FocusSuccessCode.java @@ -0,0 +1,21 @@ +package com.Timo.Timo.domain.focus.exception; + +import org.springframework.http.HttpStatus; + +import com.Timo.Timo.global.exception.code.BaseSuccessCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FocusSuccessCode implements BaseSuccessCode { + + GET_FOCUS_TODO(HttpStatus.OK, "집중 모드 TODO 조회 성공"), + NO_TODO_TODAY(HttpStatus.OK, "오늘의 TODO가 없습니다."), + ALL_TODO_COMPLETED(HttpStatus.OK, "오늘의 TODO를 모두 완료했습니다."), + ; + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/Timo/Timo/domain/focus/service/FocusService.java b/src/main/java/com/Timo/Timo/domain/focus/service/FocusService.java new file mode 100644 index 0000000..7b078b8 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/focus/service/FocusService.java @@ -0,0 +1,59 @@ +package com.Timo.Timo.domain.focus.service; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.Timo.Timo.domain.focus.dto.FocusTodoResult; +import com.Timo.Timo.domain.focus.dto.response.FocusTodoResponse; +import com.Timo.Timo.domain.focus.exception.FocusSuccessCode; +import com.Timo.Timo.domain.home.dto.response.HomeResponse.TodoResponse; +import com.Timo.Timo.domain.home.service.HomeTodoReader; +import com.Timo.Timo.domain.home.service.HomeTodoReader.LoadedTodos; +import com.Timo.Timo.domain.user.entity.User; +import com.Timo.Timo.domain.user.exception.UserErrorCode; +import com.Timo.Timo.domain.user.repository.UserRepository; +import com.Timo.Timo.global.exception.CustomException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FocusService { + + private final UserRepository userRepository; + private final HomeTodoReader homeTodoReader; + + public FocusTodoResult getFocusTodo(Long userId) { + User user = getUser(userId); + LocalDate today = LocalDate.now(ZoneId.of(user.getZoneId())); + + LoadedTodos loaded = homeTodoReader.load(userId, today, today); + List todos = homeTodoReader.sortedTodosOn(loaded, today); + + if (todos.isEmpty()) { + return new FocusTodoResult(FocusSuccessCode.NO_TODO_TODAY, FocusTodoResponse.empty(today)); + } + + return todos.stream() + .filter(todo -> !todo.completed()) + .findFirst() + .map(todo -> new FocusTodoResult( + FocusSuccessCode.GET_FOCUS_TODO, + FocusTodoResponse.of(today, todo) + )) + .orElseGet(() -> new FocusTodoResult( + FocusSuccessCode.ALL_TODO_COMPLETED, + FocusTodoResponse.empty(today) + )); + } + + private User getUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); + } +}