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

[Feature] Goal API 작성 #27 #32

Merged
merged 16 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d862204
refactor : Goal의 Tag 타입 프로퍼티 tagId의 이름을 tag로 변경 (#27)
binary-ho Dec 14, 2023
a5ac5bb
refactor : SecrityUtil의 currentMemberId 메서드의 이름을 유저 객체 이름에 맞게 current…
binary-ho Dec 14, 2023
bc36ad8
feat : Goal 단건 조회, 전체 조회 API 구현 (#27)
binary-ho Dec 15, 2023
3efce0d
chore : GoalRepository 메서드 체이닝 줄 바꿈 변경 (#27)
binary-ho Dec 15, 2023
7b709bf
Merge branch 'develop' into feature/#27
binary-ho Dec 15, 2023
41584d8
refactor : Goal의 description에 Nullable 속성 추가 (#27)
binary-ho Dec 15, 2023
1031ecc
feat : year와 month를 표현하는 String을 통해 LocalDate를 생성하는 RaemianLocalDate …
binary-ho Dec 15, 2023
3ae62ab
feat : Goal Create를 위한 User, Sticker, Tag getById 메서드 구현 (#27)
binary-ho Dec 15, 2023
ebc4526
feat : Goal Create, Delete 메서드 구현 (#27)
binary-ho Dec 15, 2023
8c9721e
test : Goal 조회 테스트 코드 작성 (#27)
binary-ho Dec 15, 2023
cec85c4
feat : GoalController 구현 (#27)
binary-ho Dec 15, 2023
71b12ff
chore : ktlint formatting (#27)
binary-ho Dec 15, 2023
2c0c07c
chore : GoalController PathVariable을 카멜 케이스로 변경 (#27)
binary-ho Dec 15, 2023
dd6e83b
test : GoalServiceTest 잘못된 부분 수정 (#27)
binary-ho Dec 15, 2023
905a95a
refactor : Delete 요청 성공시 204 No Content 응답을 내리도록 변경 (#27)
binary-ho Dec 15, 2023
e876db7
refactor : description이 null인 경우 ""가 채워지도록 변경 (#27)
binary-ho Dec 15, 2023
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
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,
)
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(
Copy link
Member

Choose a reason for hiding this comment

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

delete의 경우 http status noContent로 반환해도 좋을 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

반영했습니다 감사합니다 ㅎㅎ

@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)
}
}
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
Loading