From 6eacfee3c85ec0a8251e29bf8b14eeaa7168c7a4 Mon Sep 17 00:00:00 2001 From: cccyyy333 Date: Mon, 16 Feb 2026 20:09:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat=20:=20=ED=8F=B4=EB=8D=94=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B4=EB=8F=99api=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../folder/controller/FolderController.java | 42 +++++++++++++++ .../com/umc/timeto/folder/entity/Folder.java | 5 ++ .../folder/repository/FolderRepository.java | 2 + .../timeto/folder/service/FolderService.java | 2 + .../folder/service/FolderServiceImpl.java | 54 ++++++++++++++++++- .../global/apiPayload/code/ErrorCode.java | 1 + 6 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/umc/timeto/folder/controller/FolderController.java b/src/main/java/com/umc/timeto/folder/controller/FolderController.java index 0568b7a..a9ff072 100644 --- a/src/main/java/com/umc/timeto/folder/controller/FolderController.java +++ b/src/main/java/com/umc/timeto/folder/controller/FolderController.java @@ -76,4 +76,46 @@ public ResponseEntity deleteFolder(@PathVariable Long folderId, Authenticatio .status(ResponseCode.SUCCESS_DELETE_FOLDER.getStatus().value()) .body(new ResponseDTO<>(ResponseCode.SUCCESS_DELETE_FOLDER, null)); } + + @Operation( + summary = "폴더 이동", + description = """ + 폴더를 드래그&드롭으로 이동했을 때 변경된 인덱스를 반영합니다. + 프론트엔드는 이동 후의 최종 인덱스(newIndex)를 전달합니다. + newIndex는 0 이상 현재 폴더 개수 미만이어야 합니다. + + 예시) + 기존 순서: + 0 A + 1 B + 2 C + 3 D + + B를 맨 아래(3번 인덱스)로 이동하는 경우: + 요청 → newIndex = 3 + + 변경 결과: + 0 A + 1 C + 2 D + 3 B + """ + ) + @PatchMapping("/folder/{folderId}/move") + public ResponseEntity> moveFolder( + @PathVariable Long folderId, + @RequestParam Integer newIndex, + Authentication authentication + ) { + + Long memberId = getMemberId(authentication); + + folderService.moveFolder(folderId, memberId, newIndex); + + return ResponseEntity + .status(ResponseCode.SUCCESS_UPDATE_FOLDER.getStatus().value()) + .body(new ResponseDTO<>(ResponseCode.SUCCESS_UPDATE_FOLDER, null)); + } + + } diff --git a/src/main/java/com/umc/timeto/folder/entity/Folder.java b/src/main/java/com/umc/timeto/folder/entity/Folder.java index a54340a..335b585 100644 --- a/src/main/java/com/umc/timeto/folder/entity/Folder.java +++ b/src/main/java/com/umc/timeto/folder/entity/Folder.java @@ -18,6 +18,9 @@ public class Folder { @Column(nullable = false) private String name; + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "goal_id", nullable = false) private Goal goal; @@ -30,4 +33,6 @@ public Folder(String name, Goal goal) { public void updateName(String name) { this.name = name; } + + public void updateSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } } diff --git a/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java b/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java index 1614d4f..5752957 100644 --- a/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java +++ b/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java @@ -12,5 +12,7 @@ public interface FolderRepository extends JpaRepository { List findAllByGoal(Goal goal); Optional findByFolderIdAndGoal_Member_MemberId(Long folderId, Long memberId); + List findAllByGoalOrderBySortOrderAsc(Goal goal); + Optional findTopByGoalOrderBySortOrderDesc(Goal goal); } \ No newline at end of file diff --git a/src/main/java/com/umc/timeto/folder/service/FolderService.java b/src/main/java/com/umc/timeto/folder/service/FolderService.java index 1d430cd..9bfaca9 100644 --- a/src/main/java/com/umc/timeto/folder/service/FolderService.java +++ b/src/main/java/com/umc/timeto/folder/service/FolderService.java @@ -16,4 +16,6 @@ public interface FolderService { FolderResponseDTO updateFolder(Long folderId, FolderUpdateDTO dto, Long memberId); void deleteFolder(Long folderId, Long memberId); + + void moveFolder(Long folderId, Long memberId, Integer newIndex); } \ No newline at end of file diff --git a/src/main/java/com/umc/timeto/folder/service/FolderServiceImpl.java b/src/main/java/com/umc/timeto/folder/service/FolderServiceImpl.java index a430f46..319615f 100644 --- a/src/main/java/com/umc/timeto/folder/service/FolderServiceImpl.java +++ b/src/main/java/com/umc/timeto/folder/service/FolderServiceImpl.java @@ -41,7 +41,18 @@ public FolderResponseDTO addFolder(Long goalId, FolderAddDTO dto, Long memberId) throw new GlobalException(ErrorCode.GOAL_FORBIDDEN); } - Folder folder = folderRepository.save(dto.toEntity(goal)); + Integer nextOrder = folderRepository + .findTopByGoalOrderBySortOrderDesc(goal) + .map(folder -> folder.getSortOrder() + 1) + .orElse(0); + + Folder folder = Folder.builder() + .name(dto.getFolderName()) + .goal(goal) + .sortOrder(nextOrder) + .build(); + + folderRepository.save(folder); return FolderResponseDTO.builder() .id(folder.getFolderId()) @@ -49,6 +60,7 @@ public FolderResponseDTO addFolder(Long goalId, FolderAddDTO dto, Long memberId) .build(); } + @Override @Transactional(readOnly = true) public List getFolderList(Long goalId, Long memberId) { @@ -68,7 +80,7 @@ public List getFolderList(Long goalId, Long memberId) { FolderTodoCountProjection::getCnt )); - return folderRepository.findAllByGoal(goal) + return folderRepository.findAllByGoalOrderBySortOrderAsc(goal) .stream() .map(folder -> FolderListResponseDTO.builder() .id(folder.getFolderId()) @@ -106,6 +118,44 @@ public void deleteFolder(Long folderId, Long memberId) { throw new GlobalException(ErrorCode.GOAL_FORBIDDEN); } + Goal goal = folder.getGoal(); + folderRepository.delete(folder); + + List folders = + folderRepository.findAllByGoalOrderBySortOrderAsc(goal); + + for (int i = 0; i < folders.size(); i++) { + folders.get(i).updateSortOrder(i); + } + } + + + @Override + public void moveFolder(Long folderId, Long memberId, Integer newIndex) { + + Folder target = folderRepository.findById(folderId) + .orElseThrow(() -> new GlobalException(ErrorCode.FOLDER_NOT_FOUND)); + + if (!target.getGoal().getMember().getMemberId().equals(memberId)) { + throw new GlobalException(ErrorCode.GOAL_FORBIDDEN); + } + + Goal goal = target.getGoal(); + + List folders = + folderRepository.findAllByGoalOrderBySortOrderAsc(goal); + + if (newIndex < 0 || newIndex >= folders.size()) { + throw new GlobalException(ErrorCode.INVALID_INDEX); + } + + folders.remove(target); + folders.add(newIndex, target); + + for (int i = 0; i < folders.size(); i++) { + folders.get(i).updateSortOrder(i); + } } + } 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 6245a3e..2526e3e 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 @@ -13,6 +13,7 @@ public enum ErrorCode { */ BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), BLOCK_TIME_CONFLICT(HttpStatus.BAD_REQUEST, "이미 해당 시간에 블록이 존재합니다."), + INVALID_INDEX(HttpStatus.BAD_REQUEST, "인덱스 범위를 벗어났습니다"), /** From e85c5d42829cdcc465da04531d84c010ba27e389 Mon Sep 17 00:00:00 2001 From: cccyyy333 Date: Mon, 16 Feb 2026 21:41:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20Docs=20:=20dto=20schema=20an?= =?UTF-8?q?notation,Folder/Block=20api=20docs=20=EC=B6=94=EA=B0=80=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block/controller/BlockController.java | 28 ++-- .../block/controller/BlockControllerDocs.java | 139 ++++++++++++++++ .../com/umc/timeto/block/dto/BlockAddDTO.java | 13 +- .../timeto/block/dto/BlockResponseDTO.java | 16 ++ .../block/dto/BlockResponseDetailDTO.java | 24 ++- .../timeto/block/dto/BlockResponseNumDTO.java | 8 +- .../folder/controller/FolderController.java | 49 ++---- .../controller/FolderControllerDocs.java | 156 ++++++++++++++++++ .../umc/timeto/folder/dto/FolderAddDTO.java | 5 +- .../folder/dto/FolderListResponseDTO.java | 5 + .../timeto/folder/dto/FolderResponseDTO.java | 6 +- .../timeto/folder/dto/FolderUpdateDTO.java | 5 +- .../folder/repository/FolderRepository.java | 1 - 13 files changed, 393 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/umc/timeto/block/controller/BlockControllerDocs.java create mode 100644 src/main/java/com/umc/timeto/folder/controller/FolderControllerDocs.java diff --git a/src/main/java/com/umc/timeto/block/controller/BlockController.java b/src/main/java/com/umc/timeto/block/controller/BlockController.java index 3a192a8..b05a22b 100644 --- a/src/main/java/com/umc/timeto/block/controller/BlockController.java +++ b/src/main/java/com/umc/timeto/block/controller/BlockController.java @@ -1,11 +1,12 @@ package com.umc.timeto.block.controller; 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.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; @@ -21,7 +22,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/block") -public class BlockController { +public class BlockController implements BlockControllerDocs { private final BlockService blockService; @@ -29,10 +30,9 @@ 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( + @Override + public ResponseEntity> createBlock( @PathVariable Long todoId, @RequestBody BlockAddDTO req, Authentication authentication @@ -44,9 +44,10 @@ public ResponseEntity createBlock( .body(new ResponseDTO<>(ResponseCode.SUCCESS_ADD_BLOCK, res)); } - @Operation(summary = "날짜별 타임블럭 조회", description = "입력받은 날짜(yyyy-MM-DD)에 생성된 타임 블럭들을 조회합니다 블록 조회 기본(메인) 화면에 사용합니다. ") + @GetMapping("/day") - public ResponseEntity getBlockByDay( + @Override + public ResponseEntity>> getBlockByDay( // 기본 format: yyyy-MM-DD @RequestParam LocalDate date, Authentication authentication @@ -58,9 +59,9 @@ public ResponseEntity getBlockByDay( .body(new ResponseDTO<>(ResponseCode.SUCCESS_GET_BLOCKLIST, res)); } - @Operation(summary = "한달 날짜별 타임블럭 수", - description = "입력받은 날짜(YYYY-MM)에 생성된 타임 블럭 수를 조회합니다") + @GetMapping("/month") + @Override public ResponseEntity>> getBlockNumByMonth( @RequestParam @DateTimeFormat(pattern = "yyyy-MM") @@ -75,9 +76,9 @@ public ResponseEntity>> getBlockNumByMonth .body(new ResponseDTO<>(ResponseCode.SUCCESS_GET_BLOCK_NUMBER, res)); } - @Operation(summary = "블록 소요시간 변경", - description = "블록 소요시간 변경 시 사용합니다. 할 일 내부에서만 소요시간 변경이 가능하다면 사용하지 않아도 됩니다. ") + @PatchMapping("/{blockId}/duration") + @Override public ResponseEntity> updateDuration( @PathVariable Long blockId, @RequestParam LocalTime duration, @@ -96,10 +97,9 @@ public ResponseEntity> updateDuration( } - @Operation(summary = "블록 이동", - description = "블록을 드래그&드롭으로 이동했을 때 정보를 갱신합니다. 변경된 시작 시간을 입력으로 받습니다. " + - "이동된 시간이 다른 일정과 겹칠 경우 갱신되지 않습니다. startAt format: yyyy-MM-dd'T'HH:mm") + @PatchMapping("/{blockId}/move") + @Override public ResponseEntity> moveBlock( @PathVariable Long blockId, @RequestParam diff --git a/src/main/java/com/umc/timeto/block/controller/BlockControllerDocs.java b/src/main/java/com/umc/timeto/block/controller/BlockControllerDocs.java new file mode 100644 index 0000000..892fcd4 --- /dev/null +++ b/src/main/java/com/umc/timeto/block/controller/BlockControllerDocs.java @@ -0,0 +1,139 @@ +package com.umc.timeto.block.controller; + +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.global.apiPayload.dto.ResponseDTO; +import io.swagger.v3.oas.annotations.Operation; +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; +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; + +public interface BlockControllerDocs { + @Operation(summary = "타임블럭 저장", description = "할 일 시작 시간을 받아서 블록을 저장합니다. 블록에는 일정 겹침 검사가 존재합니다." + + "(ex: 일정 1이 7:30~8:30 일떄, 일정2의 생성/이동은 8:30분 이상부터 가능)") + @ApiResponses({ + @ApiResponse( + responseCode = "201", + description = "블록을 성공적으로 등록했습니다.", + content = @Content( + mediaType = "application/json" + ) + ), + @ApiResponse( + responseCode = "400", + description = "이미 해당 시간에 블록이 존재합니다.", + content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 할 일이 존재하지 않습니다.", + content = @Content(schema = @Schema(hidden = true)) + ) + }) + @PatchMapping("/{todoId}") + ResponseEntity> createBlock( + @PathVariable Long todoId, + @RequestBody BlockAddDTO req, + Authentication authentication + ); + + + + @Operation(summary = "날짜별 타임블럭 조회", description = "입력받은 날짜(yyyy-MM-DD)에 생성된 타임 블럭들을 조회합니다 블록 조회 기본(메인) 화면에 사용합니다. ") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "블록 리스트를 성공적으로 불러왔습니다." + ) + }) + @GetMapping("/day") + ResponseEntity>> getBlockByDay( + // 기본 format: yyyy-MM-DD + @RequestParam LocalDate date, + Authentication authentication + ); + + @Operation(summary = "한달 날짜별 타임블럭 수", + description = "입력받은 날짜(YYYY-MM)에 생성된 타임 블럭 수를 조회합니다") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "블록 수를 성공적으로 불러왔습니다." + ) + }) + @GetMapping("/month") + ResponseEntity>> getBlockNumByMonth( + @RequestParam + @DateTimeFormat(pattern = "yyyy-MM") + YearMonth yearMonth, + Authentication authentication + ); + + @Operation(summary = "블록 소요시간 변경", + description = "블록 소요시간 변경 시 사용합니다. 할 일 내부에서만 소요시간 변경이 가능하다면 사용하지 않아도 됩니다. ") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "블록을 성공적으로 업데이트했습니다" + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 블록이 존재하지 않습니다.", + content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "400", + description = "이미 해당 시간에 블록이 존재합니다." + ,content = @Content(schema = @Schema(hidden = true)) + ) + + }) + @PatchMapping("/{blockId}/duration") + ResponseEntity> updateDuration( + @PathVariable Long blockId, + @RequestParam LocalTime duration, + Authentication authentication + ); + + @Operation(summary = "블록 이동", + description = "블록을 드래그&드롭으로 이동했을 때 정보를 갱신합니다. 변경된 시작 시간을 입력으로 받습니다. " + + "이동된 시간이 다른 일정과 겹칠 경우 갱신되지 않습니다. startAt format: yyyy-MM-dd'T'HH:mm") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "블록을 성공적으로 업데이트했습니다" + ), + @ApiResponse( + responseCode = "400", + description = "이미 해당 시간에 블록이 존재합니다." + ,content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 블록이 존재하지 않습니다." + ,content = @Content(schema = @Schema(hidden = true)) + ) + + }) + @PatchMapping("/{blockId}/move") + ResponseEntity> moveBlock( + @PathVariable Long blockId, + @RequestParam + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + LocalDateTime startAt, + Authentication authentication + ); +} diff --git a/src/main/java/com/umc/timeto/block/dto/BlockAddDTO.java b/src/main/java/com/umc/timeto/block/dto/BlockAddDTO.java index ee90040..1e6666f 100644 --- a/src/main/java/com/umc/timeto/block/dto/BlockAddDTO.java +++ b/src/main/java/com/umc/timeto/block/dto/BlockAddDTO.java @@ -1,7 +1,8 @@ package com.umc.timeto.block.dto; -import jakarta.validation.constraints.NotBlank; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -10,12 +11,20 @@ import java.time.LocalDateTime; +@Schema(description = "블록 생성 요청 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class BlockAddDTO { - @NotBlank(message = "startAt은 필수 입력 값입니다.") + + @Schema( + description = "블록 시작 시간", + example = "2026-02-16T14:00", + type = "string", + pattern = "yyyy-MM-dd'T'HH:mm" + ) + @NotNull(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 index e2ed89b..3d64251 100644 --- a/src/main/java/com/umc/timeto/block/dto/BlockResponseDTO.java +++ b/src/main/java/com/umc/timeto/block/dto/BlockResponseDTO.java @@ -1,4 +1,5 @@ package com.umc.timeto.block.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -6,13 +7,28 @@ import java.time.LocalDateTime; +@Schema(description = "블록 생성/수정 응답 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class BlockResponseDTO { + + @Schema(description = "블록 ID", example = "10") private Long blockId; + + @Schema(description = "연결된 할 일 ID", example = "5") private Long todoId; + + @Schema( + description = "블록 시작 시간", + example = "2026-02-16T14:00" + ) private LocalDateTime startAt; + + @Schema( + description = "블록 종료 시간", + example = "2026-02-16T16:00" + ) 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 index 0c77669..3de849b 100644 --- a/src/main/java/com/umc/timeto/block/dto/BlockResponseDetailDTO.java +++ b/src/main/java/com/umc/timeto/block/dto/BlockResponseDetailDTO.java @@ -2,6 +2,7 @@ import com.umc.timeto.todo.domain.enums.TodoPriority; import com.umc.timeto.todo.domain.enums.TodoState; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -9,20 +10,37 @@ import java.time.LocalDateTime; +@Schema(description = "블록 상세 조회 응답 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class BlockResponseDetailDTO { + + @Schema(description = "블록 ID", example = "10") private Long blockId; + + @Schema(description = "할 일 ID", example = "5") private Long todoId; + + @Schema(description = "블록 시작 시간", example = "2026-02-16T14:00") private LocalDateTime startAt; + + @Schema(description = "블록 종료 시간", example = "2026-02-16T16:00") private LocalDateTime endAt; + + @Schema(description = "할 일 이름", example = "Spring 정렬 로직 구현") private String todoName; + + @Schema(description = "할 일 우선순위", example = "HIGH") private TodoPriority priority; + + @Schema(description = "할 일 상태", example = "progress") private TodoState state; - private String goalName; - private String color; + @Schema(description = "목표 이름", example = "백엔드 프로젝트") + private String goalName; -} + @Schema(description = "목표 색상", example = "red") + private String color; +} \ No newline at end of file diff --git a/src/main/java/com/umc/timeto/block/dto/BlockResponseNumDTO.java b/src/main/java/com/umc/timeto/block/dto/BlockResponseNumDTO.java index 85e6384..fbd319a 100644 --- a/src/main/java/com/umc/timeto/block/dto/BlockResponseNumDTO.java +++ b/src/main/java/com/umc/timeto/block/dto/BlockResponseNumDTO.java @@ -1,6 +1,7 @@ package com.umc.timeto.block.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -8,11 +9,16 @@ import java.time.LocalDate; +@Schema(description = "날짜별 블록 개수 응답 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class BlockResponseNumDTO { + + @Schema(description = "날짜", example = "2026-02-16") private LocalDate date; + + @Schema(description = "해당 날짜의 블록 개수", example = "3") private Long count; -} +} \ No newline at end of file diff --git a/src/main/java/com/umc/timeto/folder/controller/FolderController.java b/src/main/java/com/umc/timeto/folder/controller/FolderController.java index a9ff072..56a12f8 100644 --- a/src/main/java/com/umc/timeto/folder/controller/FolderController.java +++ b/src/main/java/com/umc/timeto/folder/controller/FolderController.java @@ -4,18 +4,18 @@ import com.umc.timeto.folder.service.FolderService; import com.umc.timeto.global.apiPayload.code.ResponseCode; import com.umc.timeto.global.apiPayload.dto.ResponseDTO; -import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/api") -public class FolderController { +public class FolderController implements FolderControllerDocs { private final FolderService folderService; @@ -23,9 +23,10 @@ private Long getMemberId(Authentication authentication) { return (Long) authentication.getPrincipal(); } - @Operation(summary = "목표별 폴더 리스트", description = "인증된 사용자의 목표별 폴더를 조회합니다") + @GetMapping("/goal/folder/list") - public ResponseEntity getFolderList(@RequestParam Long goalId, Authentication authentication) { + @Override + public ResponseEntity>> getFolderList(@RequestParam Long goalId, Authentication authentication) { Long memberId = getMemberId(authentication); var res = folderService.getFolderList(goalId, memberId); @@ -35,9 +36,9 @@ public ResponseEntity getFolderList(@RequestParam Long goalId, Authentication .body(new ResponseDTO<>(ResponseCode.SUCCESS_GET_FOLDERLIST, res)); } - @Operation(summary = "폴더 추가", description = "인증된 사용자가 새로운 폴더를 생성합니다") @PostMapping("/folder") - public ResponseEntity addFolder( + @Override + public ResponseEntity> addFolder( @RequestParam Long goalId, @Valid @RequestBody FolderAddDTO dto, Authentication authentication @@ -50,9 +51,10 @@ public ResponseEntity addFolder( .body(new ResponseDTO<>(ResponseCode.SUCCESS_ADD_FOLDER, res)); } - @Operation(summary = "폴더 수정", description = "인증된 사용자가 폴더를 수정합니다") + @PatchMapping("/folder/{folderId}") - public ResponseEntity updateFolder( + @Override + public ResponseEntity> updateFolder( @PathVariable Long folderId, @Valid @RequestBody FolderUpdateDTO dto, Authentication authentication @@ -65,9 +67,9 @@ public ResponseEntity updateFolder( .body(new ResponseDTO<>(ResponseCode.SUCCESS_UPDATE_FOLDER, res)); } - @Operation(summary = "폴더 삭제", description = "인증된 사용자가 폴더를 삭제합니다") @DeleteMapping("/folder/{folderId}") - public ResponseEntity deleteFolder(@PathVariable Long folderId, Authentication authentication) { + @Override + public ResponseEntity> deleteFolder(@PathVariable Long folderId, Authentication authentication) { Long memberId = getMemberId(authentication); folderService.deleteFolder(folderId, memberId); @@ -77,31 +79,8 @@ public ResponseEntity deleteFolder(@PathVariable Long folderId, Authenticatio .body(new ResponseDTO<>(ResponseCode.SUCCESS_DELETE_FOLDER, null)); } - @Operation( - summary = "폴더 이동", - description = """ - 폴더를 드래그&드롭으로 이동했을 때 변경된 인덱스를 반영합니다. - 프론트엔드는 이동 후의 최종 인덱스(newIndex)를 전달합니다. - newIndex는 0 이상 현재 폴더 개수 미만이어야 합니다. - - 예시) - 기존 순서: - 0 A - 1 B - 2 C - 3 D - - B를 맨 아래(3번 인덱스)로 이동하는 경우: - 요청 → newIndex = 3 - - 변경 결과: - 0 A - 1 C - 2 D - 3 B - """ - ) @PatchMapping("/folder/{folderId}/move") + @Override public ResponseEntity> moveFolder( @PathVariable Long folderId, @RequestParam Integer newIndex, diff --git a/src/main/java/com/umc/timeto/folder/controller/FolderControllerDocs.java b/src/main/java/com/umc/timeto/folder/controller/FolderControllerDocs.java new file mode 100644 index 0000000..66cd7e4 --- /dev/null +++ b/src/main/java/com/umc/timeto/folder/controller/FolderControllerDocs.java @@ -0,0 +1,156 @@ +package com.umc.timeto.folder.controller; + +import com.umc.timeto.folder.dto.*; +import com.umc.timeto.global.apiPayload.dto.ResponseDTO; +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; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +public interface FolderControllerDocs { + + @Operation( + summary = "목표별 폴더 리스트 조회", + description = "인증된 사용자의 특정 목표(goalId)에 속한 폴더 리스트를 조회합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "폴더 리스트를 성공적으로 불러왔습니다." + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 목표가 존재하지 않습니다." + ,content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "403", + description = "본인이 작성한 목표가 아닙니다." + ,content = @Content(schema = @Schema(hidden = true)) + ) + }) + ResponseEntity>> getFolderList( + @Parameter(description = "목표 ID", example = "1") + @RequestParam Long goalId, + Authentication authentication + ); + + + @Operation(summary = "폴더 추가", description = "새로운 폴더를 생성합니다.") + @ApiResponses({ + @ApiResponse( + responseCode = "201", + description = "폴더를 성공적으로 등록했습니다." + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 목표가 존재하지 않습니다." + ,content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "403", + description = "본인이 작성한 목표가 아닙니다." + ,content = @Content(schema = @Schema(hidden = true)) + ) + }) + ResponseEntity> addFolder( + @Parameter(description = "목표 ID", example = "1") + @RequestParam Long goalId, + @Valid @RequestBody FolderAddDTO dto, + Authentication authentication + ); + + + @Operation(summary = "폴더 수정", description = "기존 폴더의 이름을 수정합니다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "폴더를 성공적으로 수정했습니다." + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 폴더가 존재하지 않습니다." + ,content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "403", + description = "본인이 작성한 목표가 아닙니다." + ,content = @Content(schema = @Schema(hidden = true)) + ) + }) + ResponseEntity> updateFolder( + @Parameter(description = "폴더 ID", example = "10") + @PathVariable Long folderId, + @Valid @RequestBody FolderUpdateDTO dto, + Authentication authentication + ); + + + @Operation(summary = "폴더 삭제", description = "폴더를 삭제하고 정렬 순서를 재정렬합니다.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "폴더를 성공적으로 삭제했습니다." + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 폴더가 존재하지 않습니다." + ,content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "403", + description = "본인이 작성한 목표가 아닙니다." + ,content = @Content(schema = @Schema(hidden = true)) + ) + }) + ResponseEntity> deleteFolder( + @Parameter(description = "폴더 ID", example = "10") + @PathVariable Long folderId, + Authentication authentication + ); + + + @Operation( + summary = "폴더 이동", + description = """ + 폴더를 드래그&드롭으로 이동합니다. + newIndex는 0 이상 현재 폴더 개수 미만이어야 합니다. + """ + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "폴더를 성공적으로 수정했습니다." + ), + @ApiResponse( + responseCode = "400", + description = "인덱스 범위를 벗어났습니다." + ,content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "403", + description = "본인이 작성한 목표가 아닙니다." + ,content = @Content(schema = @Schema(hidden = true)) + ), + @ApiResponse( + responseCode = "404", + description = "해당 아이디를 가진 폴더가 존재하지 않습니다." + ,content = @Content(schema = @Schema(hidden = true)) + ) + }) + ResponseEntity> moveFolder( + @Parameter(description = "폴더 ID", example = "10") + @PathVariable Long folderId, + @Parameter(description = "이동할 인덱스", example = "2") + @RequestParam Integer newIndex, + Authentication authentication + ); +} diff --git a/src/main/java/com/umc/timeto/folder/dto/FolderAddDTO.java b/src/main/java/com/umc/timeto/folder/dto/FolderAddDTO.java index 7a2adc5..a1bc000 100644 --- a/src/main/java/com/umc/timeto/folder/dto/FolderAddDTO.java +++ b/src/main/java/com/umc/timeto/folder/dto/FolderAddDTO.java @@ -3,19 +3,20 @@ import com.umc.timeto.folder.entity.Folder; import com.umc.timeto.goal.entity.Goal; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; - +@Schema(description = "폴더 생성 요청 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class FolderAddDTO { - + @Schema(description = "폴더 이름", example = "백엔드 공부") @NotBlank(message = "folderName은 필수 입력 값입니다.") private String folderName; diff --git a/src/main/java/com/umc/timeto/folder/dto/FolderListResponseDTO.java b/src/main/java/com/umc/timeto/folder/dto/FolderListResponseDTO.java index 4136285..0435c83 100644 --- a/src/main/java/com/umc/timeto/folder/dto/FolderListResponseDTO.java +++ b/src/main/java/com/umc/timeto/folder/dto/FolderListResponseDTO.java @@ -1,16 +1,21 @@ package com.umc.timeto.folder.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +@Schema(description = "폴더 목록 응답 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class FolderListResponseDTO { + @Schema(description = "폴더 ID", example = "1") private Long id; + @Schema(description = "폴더 이름", example = "백엔드 공부") private String name; + @Schema(description = "진행 중인 할 일 개수", example = "3") private long ingTodoCount; } diff --git a/src/main/java/com/umc/timeto/folder/dto/FolderResponseDTO.java b/src/main/java/com/umc/timeto/folder/dto/FolderResponseDTO.java index a7ad8b1..4dd59d6 100644 --- a/src/main/java/com/umc/timeto/folder/dto/FolderResponseDTO.java +++ b/src/main/java/com/umc/timeto/folder/dto/FolderResponseDTO.java @@ -1,18 +1,20 @@ package com.umc.timeto.folder.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; - +@Schema(description = "폴더 추가/수정 시 응답 확인용 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class FolderResponseDTO { - + @Schema(description = "폴더 ID", example = "1") private Long id; + @Schema(description = "폴더 이름", example = "백엔드 공부") private String name; } \ No newline at end of file diff --git a/src/main/java/com/umc/timeto/folder/dto/FolderUpdateDTO.java b/src/main/java/com/umc/timeto/folder/dto/FolderUpdateDTO.java index c4d0c25..2bbb601 100644 --- a/src/main/java/com/umc/timeto/folder/dto/FolderUpdateDTO.java +++ b/src/main/java/com/umc/timeto/folder/dto/FolderUpdateDTO.java @@ -1,17 +1,18 @@ package com.umc.timeto.folder.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; - +@Schema(description = "폴더 이름 업데이트 요청 DTO") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class FolderUpdateDTO { - + @Schema(description = "폴더 이름", example = "백엔드 공부") @NotBlank(message = "folderName은 필수 입력 값입니다.") private String folderName; } diff --git a/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java b/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java index 5752957..46c7064 100644 --- a/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java +++ b/src/main/java/com/umc/timeto/folder/repository/FolderRepository.java @@ -10,7 +10,6 @@ @Repository public interface FolderRepository extends JpaRepository { - List findAllByGoal(Goal goal); Optional findByFolderIdAndGoal_Member_MemberId(Long folderId, Long memberId); List findAllByGoalOrderBySortOrderAsc(Goal goal); Optional findTopByGoalOrderBySortOrderDesc(Goal goal);