-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] #78 - 집중 모드 #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<BaseResponse<FocusTodoResponse>> getFocusTodo( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails | ||
| ) { | ||
| FocusTodoResult result = focusService.getFocusTodo(userDetails.getUserId()); | ||
|
|
||
| return ResponseEntity | ||
| .status(result.successCode().getHttpStatus()) | ||
| .body(BaseResponse.onSuccess(result.successCode(), result.response())); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<BaseResponse<FocusTodoResponse>> getFocusTodo( | ||
| @Parameter(hidden = true) CustomUserDetails userDetails | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) { } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Comment on lines
+6
to
+8
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p4) Focus 응답 DTO가 Home 응답 DTO(TodoResponse, TagResponse, SubtaskResponse)에 직접 의존하고 있어서 Home API 응답 구조가 바뀌면 Focus API에도 같이 영향이 갈 수 있지 않나 싶었어용 Home과 동일한 응답 구조를 의도적으로 재사용하는 거라면 굿굿슨이지만 Focus API를 독립적으로 가져갈 계획이라면 전용 DTO로 분리하는 것도 좋을 것 같다는 말이었어용
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. focus의 경우 home의 오늘 조회와 유사해서 재사용을 노렸습니다. 이후 focus와 관련된 api가 늘어난다면 따로분리하는게 좋을 것 같네용 |
||
| 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<SubtaskResponse> 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() | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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를 모두 완료했습니다."), | ||
| ; | ||
|
Comment on lines
+16
to
+17
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 유레카!! 저도 반영하겠습니다 |
||
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String message; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TodoResponse> todos = homeTodoReader.sortedTodosOn(loaded, today); | ||
|
|
||
| if (todos.isEmpty()) { | ||
| return new FocusTodoResult(FocusSuccessCode.NO_TODO_TODAY, FocusTodoResponse.empty(today)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO 없음 , 모두 완료 , 조회 성공 케이스를 FocusTodoResult로 successCode와 response를 같이 넘기도록 분리 깔끔하네용 PR 설명에 적힌 의도도 코드에서 잘 드러나는 것 같습니다! |
||
| } | ||
|
|
||
| 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)); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이것도 여백으로 치나요 안 치나요 상관없나요 (동글둥글당글딩글)