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

[KS-8] 할일(업무) API 구현 #20

Merged
merged 9 commits into from
May 6, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class MemberController(
fun updateMember(
@PathVariable memberId: Long,
@RequestBody memberUpdateRequest: MemberUpdateRequest,
): ResponseEntity<BaseResponse<MemberResponse>> {
): ResponseEntity<BaseResponse<Unit>> {
memberProfileService.update(memberId, memberUpdateRequest)
return BaseResponse.ok(SuccessMessage.SUCCESS_UPDATE_MEMBER)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package site.katchup.springboot.controller

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import site.katchup.springboot.dto.task.request.TaskRequest
import site.katchup.springboot.dto.task.request.TaskUpdateRequest
import site.katchup.springboot.dto.task.response.TaskResponse
import site.katchup.springboot.global.message.SuccessMessage
import site.katchup.springboot.global.response.BaseResponse
import site.katchup.springboot.service.task.TaskService

@ApiControllerV1
class TaskController(
private val taskService: TaskService,
) {

@PostMapping("/tasks")
fun addTask(
memberId: Long,
@RequestBody request: TaskRequest,
): ResponseEntity<BaseResponse<Unit>> {
taskService.addTask(memberId, request)
return BaseResponse.created(SuccessMessage.SUCCESS_ADD_TASK)
}

@PatchMapping("/tasks/{taskId}")
fun updateTask(
memberId: Long,
@RequestBody request: TaskUpdateRequest,
@PathVariable taskId: Long,
): ResponseEntity<BaseResponse<Unit>> {
taskService.updateTask(memberId, taskId, request)
return BaseResponse.ok(SuccessMessage.SUCCESS_UPDATE_TASK)
}

@GetMapping("/tasks/{taskId}")
fun getTask(
memberId: Long,
@PathVariable taskId: Long,
): ResponseEntity<BaseResponse<TaskResponse>> {
val response = taskService.getTask(memberId, taskId)
return BaseResponse.ok(SuccessMessage.SUCCESS_GET_TASK, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package site.katchup.springboot.dto.task.request

data class TaskRequest(
val title: String,
val content: String,
val workSpaceId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package site.katchup.springboot.dto.task.request

import site.katchup.springboot.exception.base.BadRequestException
import site.katchup.springboot.global.message.FailMessage

data class TaskUpdateRequest(
val title: String,
val content: String,
) {
init {
validate()
}

private fun validate() {
if (title.isBlank() && title.length > 60) {
throw BadRequestException(FailMessage.INVALID_PARAMETER)
}
if (content.isBlank() && content.length > 600) {
throw BadRequestException(FailMessage.INVALID_PARAMETER)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package site.katchup.springboot.dto.task.response

import com.fasterxml.jackson.annotation.JsonFormat
import site.katchup.springboot.entity.Task
import java.time.LocalDateTime

data class TaskResponse(
val id: Long,
val title: String,
val content: String,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
val createdAt: LocalDateTime,
) {

companion object {

fun of(
task: Task,
): TaskResponse {
return TaskResponse(
id = task.id!!,
title = task.title,
content = task.content,
createdAt = task.createdAt,
)
}
}
}
34 changes: 34 additions & 0 deletions src/main/kotlin/site/katchup/springboot/entity/Task.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package site.katchup.springboot.entity

import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import site.katchup.springboot.exception.entity.InvalidEntityArgumentException
import site.katchup.springboot.global.message.FailMessage

@Entity
class Task(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
var title: String,
var content: String,
val status: TaskStatus,
val memberWorkSpaceId: Long,
) : BaseTimeEntity() {

init {
validate()
}

private fun validate() {
if (title.length > 60) {
throw InvalidEntityArgumentException(FailMessage.INVALID_TASK_TITLE)
}

if (content.length > 600) {
throw InvalidEntityArgumentException(FailMessage.INVALID_TASK_CONTENT)
}
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/site/katchup/springboot/entity/TaskStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package site.katchup.springboot.entity

enum class TaskStatus {
IN_PROGRESS,
DONE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package site.katchup.springboot.exception.entity

import site.katchup.springboot.exception.base.BadRequestException
import site.katchup.springboot.global.message.FailMessage

class InvalidEntityArgumentException(failMessage: FailMessage) : BadRequestException(failMessage)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ package site.katchup.springboot.global.message
enum class FailMessage(
val value: String,
) {
/*
* 400 BAD REQUEST
*/

INVALID_PARAMETER("잘못된 요청 파라미터"),
INVALID_TASK_TITLE("유효하지 않은 업무 제목"),
INVALID_TASK_CONTENT("유효하지 않은 업무 내용"),

/*
* 401 UNAUTHORIZED
*/
Expand All @@ -12,6 +20,8 @@ enum class FailMessage(
* 404 NOT FOUND
*/
MEMBER_NOT_FOUND("해당 사용자를 찾을 수 없음"),
MEMBER_WORKSPACE_NOT_FOUND("해당 사용자 또는 워크스페이스를 찾을 수 없음"),
TASK_NOT_FOUND("해당 업무를 찾을 수 없음"),

/*
* 500 INTERNAL_SERVER_ERROR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ enum class SuccessMessage(
SUCCESS_UPDATE_MEMBER("사용자 정보 수정 성공"),
SUCCESS_GET_NOTIFICATIONS("알림 목록 조회 성공"),
SUCCESS_ADD_NOTIFICATION("알림 추가 성공"),
SUCCESS_ADD_TASK("업무 추가 성공"),
SUCCESS_UPDATE_TASK("업무 수정 성공"),
SUCCESS_GET_TASK("업무 조회 성공"),

;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class BaseResponse<T>(
return ResponseEntity.status(HttpStatus.CREATED).body(BaseResponse(message.value, data))
}

fun <T> created(message: SuccessMessage): ResponseEntity<BaseResponse<T>> {
return ResponseEntity.status(HttpStatus.CREATED).body(BaseResponse(message.value, null))
}

fun fail(status: HttpStatus, message: FailMessage): ResponseEntity<BaseResponse<Unit>> {
return ResponseEntity.status(status).body(BaseResponse(message.value))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package site.katchup.springboot.repository

import org.springframework.data.jpa.repository.JpaRepository
import site.katchup.springboot.entity.MemberWorkSpace

interface MemberWorkSpaceRepository : JpaRepository<MemberWorkSpace, Long> {
fun findByMemberIdAndWorkSpaceId(memberId: Long, workSpaceId: Long): MemberWorkSpace?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package site.katchup.springboot.repository

import org.springframework.data.jpa.repository.JpaRepository
import site.katchup.springboot.entity.Task

interface TaskRepository : JpaRepository<Task, Long>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package site.katchup.springboot.service.memberworkspace

import org.springframework.stereotype.Component
import site.katchup.springboot.entity.MemberWorkSpace
import site.katchup.springboot.exception.entity.EntityNotFoundException
import site.katchup.springboot.global.message.FailMessage
import site.katchup.springboot.repository.MemberWorkSpaceRepository

@Component
class MemberWorkSpaceFinder(
private val memberWorkSpaceRepository: MemberWorkSpaceRepository,
) {
fun findMemberWorkSpaceOrThrow(memberId: Long, workSpaceId: Long): MemberWorkSpace {
return memberWorkSpaceRepository.findByMemberIdAndWorkSpaceId(memberId, workSpaceId)
?: throw EntityNotFoundException(FailMessage.MEMBER_WORKSPACE_NOT_FOUND)
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/site/katchup/springboot/service/task/TaskFinder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package site.katchup.springboot.service.task

import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component
import site.katchup.springboot.entity.Task
import site.katchup.springboot.exception.entity.EntityNotFoundException
import site.katchup.springboot.global.message.FailMessage
import site.katchup.springboot.repository.TaskRepository
import java.util.*

@Component
class TaskFinder(
private val taskRepository: TaskRepository,
) {
fun findTaskOrThrow(taskId: Long): Task {
return taskRepository.findByIdOrNull(taskId) ?: throw EntityNotFoundException(FailMessage.TASK_NOT_FOUND)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package site.katchup.springboot.service.task

import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import site.katchup.springboot.dto.task.request.TaskRequest
import site.katchup.springboot.dto.task.request.TaskUpdateRequest
import site.katchup.springboot.dto.task.response.TaskResponse
import site.katchup.springboot.entity.Task
import site.katchup.springboot.entity.TaskStatus
import site.katchup.springboot.repository.TaskRepository
import site.katchup.springboot.service.memberworkspace.MemberWorkSpaceFinder

@Service
@Transactional(readOnly = true)
class TaskService(
private val taskRepository: TaskRepository,
private val memberWorkSpaceFinder: MemberWorkSpaceFinder,
private val taskFinder: TaskFinder,
) {

@Transactional
fun addTask(memberId: Long, request: TaskRequest) {
val memberWorkSpace = memberWorkSpaceFinder.findMemberWorkSpaceOrThrow(memberId, request.workSpaceId)
val task = Task(
title = request.title,
content = request.content,
status = TaskStatus.IN_PROGRESS,
memberWorkSpaceId = memberWorkSpace.id!!,
)
taskRepository.save(task)
}

@Transactional
fun updateTask(memberId: Long, taskId: Long, request: TaskUpdateRequest) {
val task = taskFinder.findTaskOrThrow(taskId)
task.title = request.title
task.content = request.content
}

fun getTask(memberId: Long, taskId: Long): TaskResponse {
val task = taskFinder.findTaskOrThrow(taskId)
return TaskResponse.of(task)
}
}
35 changes: 35 additions & 0 deletions src/test/kotlin/site/katchup/springboot/entity/TaskTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package site.katchup.springboot.entity

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import site.katchup.springboot.exception.entity.InvalidEntityArgumentException

class TaskTest : FunSpec() {

init {
test("제목이 60글자 초과인 업무는 추가할 수 없다.") {
val titleLength61 = "a".repeat(61)
shouldThrow<InvalidEntityArgumentException> {
Task(
title = titleLength61,
content = "content",
status = TaskStatus.IN_PROGRESS,
memberWorkSpaceId = 1,
)
}.message shouldBe "유효하지 않은 업무 제목"
}

test("내용이 600글자 초과된 업무는 추가할 수 없다.") {
val content = "a".repeat(601)
shouldThrow<InvalidEntityArgumentException> {
Task(
title = "title",
content = content,
status = TaskStatus.IN_PROGRESS,
memberWorkSpaceId = 1,
)
}.message shouldBe "유효하지 않은 업무 내용"
}
}
}
Loading