Skip to content
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

[#9] Task CRUD API 스펙 정의 및 생성 #10

Merged
merged 22 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2365d68
refactor: HealthController 파일 이동
olivejua Aug 16, 2024
e4ebac4
docs: README.md git flow 내용 추가
olivejua Aug 17, 2024
52e6b7a
chore: lombok 플러그인 추가
olivejua Aug 17, 2024
43aaeb8
feat: ApiResponse 클래스 생성
olivejua Aug 17, 2024
84843ea
refactor: ErrorDetail 클래스명 변경
olivejua Aug 17, 2024
d57bbc5
chore: restDocs 설정 추가
olivejua Aug 18, 2024
6f626ab
feat: Task 단건조회 API 생성
olivejua Aug 18, 2024
6942547
feat: Task 생성 API 생성
olivejua Aug 19, 2024
55639b5
feat: Task 삭제 API 생성
olivejua Aug 19, 2024
7c1a8a2
feat: ApiControllerAdvice 생성 및 실패테스트케이스 추가
olivejua Aug 19, 2024
00871e2
docs: index.adoc 작성
olivejua Aug 19, 2024
dfe4b06
style: todo 주석 제거
olivejua Aug 19, 2024
369e578
refactor: NoData 클래스 제거
olivejua Aug 19, 2024
e05ff3a
chore: bootJar태스크의 dependson 추가
olivejua Aug 19, 2024
fcaedc8
refactor: Custom Exception 제거 및 Error Code 통일
olivejua Aug 20, 2024
e50bd14
refactor: TaskControllerTest 클래스명 변경
olivejua Aug 20, 2024
39a0a26
refactor: TAskCreateRequest 의 파라미터 유효성 검증 및 테스트 추가
olivejua Aug 20, 2024
c0d6ee3
refactor: TaskUpdate API 테스트 및 실패케이스 추가
olivejua Aug 21, 2024
ac12666
style: todo 주석 제거
olivejua Aug 21, 2024
d34dbc4
test: update task 실패케이스 추가
olivejua Aug 21, 2024
f2a372e
docs: task.adoc 생성
olivejua Aug 21, 2024
90a9de0
style: TODO 코멘트 수정 및 이슈 번호 추가
olivejua Sep 2, 2024
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
# task-buddy
# task-buddy


## Git
### Work Flow
1. Issue 발생하여 TODO 등록
2. feature 브랜치 생성 (브랜치명: `feature/{Issue 번호}-{작업타이틀}`)
3. PR 제출 (제목: `[{issue 번호}] {작업 주제}`)
24 changes: 24 additions & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id "io.freefair.lombok" version "8.7.1"
id "org.asciidoctor.jvm.convert" version "3.3.2"
olivejua marked this conversation as resolved.
Show resolved Hide resolved
}

java {
Expand All @@ -15,12 +17,34 @@ repositories {
mavenCentral()
}

configurations {
asciidoctorExt
olivejua marked this conversation as resolved.
Show resolved Hide resolved
}

dependencies {
implementation project(':core')
implementation 'org.springframework.boot:spring-boot-starter-web'
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

ext {
snippetsDir = file('build/generated-snippets')
}

test {
useJUnitPlatform()
outputs.dir snippetsDir
}

asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
dependsOn test
}

bootJar {
dependsOn asciidoctor
}
10 changes: 10 additions & 0 deletions api/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
= Api Document
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 1
:sectlinks:
:docinfo: shared-head

include::/{docfile}/../task.adoc[]
58 changes: 58 additions & 0 deletions api/src/docs/asciidoc/task.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
== 1. Task

[[resources-tasks-get]]
=== 1) Task 조회

`GET` 요청을 사용해서 기존 Task 하나를 조회할 수 있다.

==== 성공 응답
operation::get-a-task-with-id/success[snippets='request-headers,path-parameters,http-request,response-headers,response-fields,http-response']

==== Task가 존재하지 않을 경우
operation::get-a-task-with-id/fail/does-not-exist[snippets='http-request,http-response']

==== 잘못된 형식의 ID로 요청한 경우
operation::get-a-task-with-id/fail/negative-id-value[snippets='http-request,http-response']


[[resources-tasks-create]]
=== 2) Task 생성

`POST` 요청을 사용해서 새 Task를 만들 수 있다.

==== 성공 응답
operation::create-a-task/success[snippets='request-headers,request-fields,http-request,response-headers,response-fields,http-response']

==== Request 조건에 만족하지 않을 경우
operation::create-a-task/fail/invalid-request-data[snippets='http-request,http-response']

[[resources-tasks-update]]
=== 3) Task 수정

`PATCH` 요청을 사용해서 기존 Task를 수정할 수 있다.

==== 성공 응답
operation::update-a-task/success[snippets='request-headers,path-parameters,request-fields,http-request,response-headers,response-fields,http-response']

==== 잘못된 형식의 ID로 요청한 경우
operation::update-a-task/fail/negative-id-value[snippets='http-request,http-response']

==== Request 조건에 만족하지 않을 경우
operation::update-a-task/fail/invalid-request-data[snippets='http-request,http-response']

==== Task가 존재하지 않을 경우
operation::update-a-task/fail/does-not-exist[snippets='http-request,http-response']

[[resources-tasks-remove]]
=== 4) Task 삭제

`DELETE` 요청을 사용해서 기존 Task를 수정할 수 있다.

==== 성공 응답
operation::remove-a-task/success[snippets='request-headers,path-parameters,http-request,response-headers,response-fields,http-response']

==== 잘못된 형식의 ID로 요청한 경우
operation::remove-a-task/fail/negative-id-value[snippets='http-request,http-response']

==== Task가 존재하지 않을 경우
operation::remove-a-task/fail/does-not-exist[snippets='http-request,http-response']
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.taskbuddy.api.controller;

import com.taskbuddy.api.controller.response.ApiResponse;
import com.taskbuddy.api.controller.response.ErrorDetail;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ApiControllerAdvice {

@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<ApiResponse<?>> handleIllegalStateException(IllegalStateException exception) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.fail(new ErrorDetail("INVALID_PARAMETER_STATE", exception.getMessage())));
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException exception) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.fail(new ErrorDetail("INVALID_PARAMETER_STATE", exception.getMessage())));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.taskbuddy.api.common;
package com.taskbuddy.api.controller;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -10,5 +10,8 @@ public class HealthController {

@ResponseStatus(code = HttpStatus.OK)
@GetMapping("/health-check")
public void healthCheck() {}
public void healthCheck() {

}

}
77 changes: 77 additions & 0 deletions api/src/main/java/com/taskbuddy/api/controller/TaskController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.taskbuddy.api.controller;

import com.taskbuddy.api.controller.request.TaskCreateRequest;
import com.taskbuddy.api.controller.request.TaskUpdateRequest;
import com.taskbuddy.api.controller.response.ApiResponse;
import com.taskbuddy.api.controller.response.task.TaskResponse;
import com.taskbuddy.api.controller.response.task.TimeFrame;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.time.LocalDateTime;

@RequestMapping("/v1/tasks")
@RestController
public class TaskController {
@GetMapping("/{id}")
ResponseEntity<ApiResponse<TaskResponse>> getTask(@PathVariable("id") Long id) {
Assert.state(id >= 0, "The id value must be positive.");

// FIXME 서비스 로직 구현하면 제거하기
if (id == 0) {
throw new IllegalArgumentException("The given task with id does not exist.");
}

//Dummy
TaskResponse response = new TaskResponse(
1L
, "알고리즘 문제 풀기"
, "백준1902..."
, false
, new TimeFrame(
LocalDateTime.of(2024, 8, 1, 0, 0, 0)
, LocalDateTime.of(2024, 8, 31, 23, 59, 59)));

return ResponseEntity
.ok(ApiResponse.success(response));
}

@PostMapping
ResponseEntity<ApiResponse<?>> createTask(@RequestBody TaskCreateRequest request) {
//Dummy
final long createdTaskId = 1L;

return ResponseEntity
.created(URI.create("localhost:8080/v1/tasks/" + createdTaskId))
.body(ApiResponse.success());
}

@PatchMapping("/{id}")
ResponseEntity<ApiResponse<?>> updateTask(@PathVariable("id") Long id, @RequestBody TaskUpdateRequest request) {
Assert.state(id >= 0, "The id value must be positive.");

// FIXME 서비스 로직 구현하면 제거하기
if (id == 0) {
throw new IllegalArgumentException("The given task with id does not exist.");
}

return ResponseEntity
.ok(ApiResponse.success());
}

@DeleteMapping("/{id}")
ResponseEntity<ApiResponse<?>> removeTask(@PathVariable("id") Long id) {
Assert.state(id >= 0, "The id value must be positive.");

// FIXME 서비스 로직 구현하면 제거하기
if (id == 0) {
throw new IllegalArgumentException("The given task with id does not exist.");
}

return ResponseEntity
.ok(ApiResponse.success());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.taskbuddy.api.controller.request;

import com.taskbuddy.api.controller.response.task.TimeFrame;
import org.springframework.util.Assert;

public record TaskCreateRequest(
String title,
String description,
TimeFrame timeFrame
) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 부분이랑 스타일이 다른 것 같네요?

Copy link
Collaborator Author

@olivejua olivejua Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spring Validation 의존성을 추가하기 전에 Request에 대한 Validation을 진행해야할 것 같아 Assert 를 추가한 유효성검증 로직을 추가하면서 생성자를 만들었습니다. Spring Validation Annotation을 붙이면 해당 생성자는 제거하려고 합니다!

public TaskCreateRequest {
Assert.state(title != null && !title.isBlank(), "The title of task must not be blank.");
Assert.state(description == null || description.length() <= 500, "The description length must be equal or less than 500");
Assert.notNull(timeFrame, "The timeFrame must not be null.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.taskbuddy.api.controller.request;

import com.taskbuddy.api.controller.response.task.TimeFrame;
import org.springframework.util.Assert;

public record TaskUpdateRequest(
String title,
String description,
TimeFrame timeFrame
) {
public TaskUpdateRequest {
Assert.state(title != null && !title.isBlank(), "The title of task must not be blank.");
Assert.state(description == null || description.length() <= 500, "The description length must be equal or less than 500");
Assert.notNull(timeFrame, "The timeFrame must not be null.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.taskbuddy.api.controller.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import org.springframework.util.Assert;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
public class ApiResponse<D> {
private final ResultStatus status;
private final D data;
private final ErrorDetail error;

private ApiResponse(ResultStatus status, D data, ErrorDetail error) {
this.status = status;
this.data = data;
this.error = error;
}

public static <D> ApiResponse<D> success(D data) {
return new ApiResponse<>(ResultStatus.SUCCESS, data, null);
}

public static ApiResponse<?> success() {
return new ApiResponse<>(ResultStatus.SUCCESS, null, null);
}

public static ApiResponse<?> fail(ErrorDetail error) {
Assert.notNull(error, "The error argument must not be null.");

return new ApiResponse<>(ResultStatus.FAIL, null, error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.taskbuddy.api.controller.response;

public record ErrorDetail(String code, String message) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.taskbuddy.api.controller.response;

public enum ResultStatus {
SUCCESS,
FAIL,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.taskbuddy.api.controller.response.task;

public record TaskResponse (
Long id,
String title,
String description,
Boolean isDone,
TimeFrame timeFrame
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.taskbuddy.api.controller.response.task;

import org.springframework.util.Assert;

import java.time.LocalDateTime;

public record TimeFrame(
LocalDateTime startDateTime,
LocalDateTime endDateTime) {
public TimeFrame {
Assert.notNull(startDateTime, "The startDateTime must not be null.");
Assert.notNull(endDateTime, "The endDateTime must not be null.");
Assert.isTrue(endDateTime.isAfter(startDateTime), "The endDateTime must be after than the startDateTime.");
}
}
Loading
Loading