Skip to content

Commit

Permalink
[Feature] Goal API 작성 #27 (#32)
Browse files Browse the repository at this point in the history
* refactor : Goal의 Tag 타입 프로퍼티 tagId의 이름을 tag로 변경 (#27)

* refactor : SecrityUtil의 currentMemberId 메서드의 이름을 유저 객체 이름에 맞게 currentUserId로 변경 (#27)

* feat : Goal 단건 조회, 전체 조회 API 구현 (#27)

* chore : GoalRepository 메서드 체이닝 줄 바꿈 변경 (#27)

* refactor : Goal의 description에 Nullable 속성 추가 (#27)

* feat : year와 month를 표현하는 String을 통해 LocalDate를 생성하는 RaemianLocalDate 구현 (#27)

* feat : Goal Create를 위한 User, Sticker, Tag getById 메서드 구현 (#27)

* feat : Goal Create, Delete 메서드 구현 (#27)

* test : Goal 조회 테스트 코드 작성 (#27)

* feat : GoalController 구현 (#27)

* chore : ktlint formatting (#27)

* chore : GoalController PathVariable을 카멜 케이스로 변경 (#27)

* test : GoalServiceTest 잘못된 부분 수정 (#27)

* refactor : Delete 요청 성공시 204 No Content 응답을 내리도록 변경 (#27)

* refactor : description이 null인 경우 ""가 채워지도록 변경 (#27)
  • Loading branch information
binary-ho authored Dec 15, 2023
1 parent e7e6e59 commit b16cffe
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.raemian.api.goal

data class CreateGoalResponse(
val id: Long,
)
62 changes: 62 additions & 0 deletions application/api/src/main/kotlin/io/raemian/api/goal/GoalService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.raemian.api.goal

import io.raemian.api.goal.controller.request.CreateGoalRequest
import io.raemian.api.goal.controller.request.DeleteGoalRequest
import io.raemian.api.goal.controller.response.GoalResponse
import io.raemian.api.goal.controller.response.GoalsResponse
import io.raemian.api.sticker.StickerService
import io.raemian.api.support.RaemianLocalDate
import io.raemian.api.tag.TagService
import io.raemian.api.user.UserService
import io.raemian.storage.db.core.goal.Goal
import io.raemian.storage.db.core.goal.GoalRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class GoalService(
private val userService: UserService,
private val stickerService: StickerService,
private val tagService: TagService,
private val goalRepository: GoalRepository,
) {

@Transactional(readOnly = true)
fun findAllByUserId(userId: Long): GoalsResponse {
val goals = goalRepository.findAllByUserId(userId)
return GoalsResponse(goals)
}

@Transactional(readOnly = true)
fun getById(id: Long): GoalResponse {
val goal = goalRepository.getById(id)
return GoalResponse(goal)
}

@Transactional
fun create(userId: Long, createGoalRequest: CreateGoalRequest): CreateGoalResponse {
val (title, yearOfDeadline, monthOfDeadLine, stickerId, tagId, description) = createGoalRequest

val deadline = RaemianLocalDate.of(yearOfDeadline, monthOfDeadLine)
val sticker = stickerService.getById(stickerId)
val tag = tagService.getById(tagId)
val user = userService.getById(userId)

val goal = Goal(user, title, deadline, sticker, tag, description!!, emptyList())
val savedGoal = goalRepository.save(goal)
return CreateGoalResponse(savedGoal.id!!)
}

@Transactional
fun delete(userId: Long, deleteGoalRequest: DeleteGoalRequest) {
val goal = goalRepository.getById(deleteGoalRequest.goalId)
validateGoalIsUsers(userId, goal)
goalRepository.delete(goal)
}

private fun validateGoalIsUsers(userId: Long, goal: Goal) {
if (userId != goal.user.id) {
throw SecurityException()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.raemian.api.goal.controller

import io.raemian.api.auth.domain.CurrentUser
import io.raemian.api.goal.CreateGoalResponse
import io.raemian.api.goal.GoalService
import io.raemian.api.goal.controller.request.CreateGoalRequest
import io.raemian.api.goal.controller.request.DeleteGoalRequest
import io.raemian.api.goal.controller.response.GoalResponse
import io.raemian.api.goal.controller.response.GoalsResponse
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.net.URI

fun String.toUri(): URI = URI.create(this)

@RestController
@RequestMapping("/goal")
class GoalController(
private val goalService: GoalService,
) {

@GetMapping
fun findAllByUserId(
@AuthenticationPrincipal currentUser: CurrentUser,
): ResponseEntity<GoalsResponse> {
val response = goalService.findAllByUserId(currentUser.id)
return ResponseEntity.ok(response)
}

@GetMapping("/{goalId}")
fun getByUserId(
@PathVariable("goalId") goalId: Long,
): ResponseEntity<GoalResponse> =
ResponseEntity.ok(goalService.getById(goalId))

@PostMapping
fun create(
@AuthenticationPrincipal currentUser: CurrentUser,
@RequestBody createGoalRequest: CreateGoalRequest,
): ResponseEntity<CreateGoalResponse> {
val response = goalService.create(currentUser.id, createGoalRequest)
return ResponseEntity
.created("/goal/${response.id}".toUri())
.body(response)
}

@DeleteMapping
fun delete(
@AuthenticationPrincipal currentUser: CurrentUser,
@RequestBody deleteGoalRequest: DeleteGoalRequest,
): ResponseEntity<Unit> {
goalService.delete(currentUser.id, deleteGoalRequest)
return ResponseEntity.noContent().build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.raemian.api.goal.controller.request

data class CreateGoalRequest(
val title: String,
val yearOfDeadline: String,
val monthOfDeadLine: String,
val stickerId: Long,
val tagId: Long,
val description: String? = "",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.raemian.api.goal.controller.request

class DeleteGoalRequest(
val goalId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.raemian.api.goal.controller.response

import io.raemian.storage.db.core.goal.Goal
import io.raemian.storage.db.core.sticker.StickerImage
import io.raemian.storage.db.core.tag.Tag
import io.raemian.storage.db.core.task.Task
import java.time.LocalDate

data class GoalResponse(
val title: String,
val deadline: LocalDate,
val sticker: StickerImage,
val tagInfo: TagInfo,
val tasks: List<TaskInfo>,
) {

constructor(goal: Goal) : this(
goal.title,
goal.deadline,
goal.sticker.stickerImage,
TagInfo(goal.tag),
goal.tasks.map(::TaskInfo),
)

data class TagInfo(
val tagId: Long?,
val tagContent: String,
) {

constructor(tag: Tag) : this(tag.id, tag.content)
}

data class TaskInfo(
val taskId: Long?,
val isTaskDone: Boolean,
val taskDescription: String,
) {

constructor(task: Task) : this(task.id, task.isDone, task.description)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.raemian.api.goal.controller.response

import io.raemian.storage.db.core.goal.Goal
import io.raemian.storage.db.core.sticker.StickerImage
import java.time.LocalDate

data class GoalsResponse(
val goals: Goals,
) {

constructor(goals: List<Goal>) : this(
Goals(
goals.map(::GoalInfo),
),
)

data class GoalInfo(
val id: Long?,
val title: String,
val deadline: LocalDate,
val sticker: StickerImage,
val tagContent: String,
val description: String? = "",
) {

constructor(goal: Goal) : this(
id = goal.id,
title = goal.title,
deadline = goal.deadline,
sticker = goal.sticker.stickerImage,
tagContent = goal.tag.content,
description = goal.description,
)
}

data class Goals(
val goalInfos: List<GoalInfo>,
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.raemian.api.sticker

import io.raemian.api.sticker.controller.response.StickerResponse
import io.raemian.storage.db.core.sticker.Sticker
import io.raemian.storage.db.core.sticker.StickerRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -15,4 +16,9 @@ class StickerService(
return stickerRepository.findAll()
.map(::StickerResponse)
}

@Transactional(readOnly = true)
fun getById(id: Long): Sticker {
return stickerRepository.getById(id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.raemian.api.support

import java.time.LocalDate
import java.time.Month
import java.time.Year

object RaemianLocalDate {

private const val DAY_OF_MONTH = 1

fun of(year: String, month: String): LocalDate {
val parsedYear = parseYear(year)
val parsedMonth = parseMonth(month)
return LocalDate.of(parsedYear, parsedMonth, DAY_OF_MONTH)
}

private fun parseYear(year: String): Int {
validateYearFormat(year)
return year.toInt()
}

private fun validateYearFormat(year: String) {
runCatching {
Year.parse(year)
}.onFailure {
throw IllegalArgumentException()
}
}

private fun parseMonth(month: String): Month {
val result = runCatching {
Month.of(month.toInt())
}

return result.getOrNull()
?: throw IllegalArgumentException()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.springframework.security.core.context.SecurityContextHolder

object SecurityUtil {
// SecurityContext 에 유저 정보가 저장되는 시점
fun currentMemberId(): Long {
fun currentUserId(): Long {
val authentication: Authentication? = SecurityContextHolder.getContext().authentication
if (authentication == null || authentication.name == null) {
throw RuntimeException("Security Context 에 인증 정보가 없습니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.raemian.api.tag

import io.raemian.api.tag.controller.response.TagResponse
import io.raemian.storage.db.core.tag.Tag
import io.raemian.storage.db.core.tag.TagRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -15,4 +16,9 @@ class TagService(
return tagRepository.findAll()
.map(::TagResponse)
}

@Transactional(readOnly = true)
fun getById(id: Long): Tag {
return tagRepository.getById(id)
}
}
17 changes: 17 additions & 0 deletions application/api/src/main/kotlin/io/raemian/api/user/UserService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.raemian.api.user

import io.raemian.storage.db.core.user.User
import io.raemian.storage.db.core.user.UserRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class UserService(
private val userRepository: UserRepository,
) {

@Transactional(readOnly = true)
fun getById(userId: Long): User {
return userRepository.getById(userId)
}
}
Loading

0 comments on commit b16cffe

Please sign in to comment.