Skip to content

Commit

Permalink
[feat #27] 포킷 CRUD API (#28)
Browse files Browse the repository at this point in the history
* feat: log dependenct μΆ”κ°€

* feat: μ˜ˆμ™Έ 클래슀 μˆ˜μ •

* feat: spring-data-commons λ””νŽœλ˜μ‹œ μΆ”κ°€

* feat: μ—”ν‹°ν‹° μˆ˜μ •

* feat: μΉ΄ν…Œκ³ λ¦¬ CRUD κ΅¬ν˜„
  • Loading branch information
jimin3263 authored Jul 22, 2024
1 parent 4789d34 commit 51cb9d9
Show file tree
Hide file tree
Showing 27 changed files with 510 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.pokit.category

import com.pokit.auth.config.ErrorOperation
import com.pokit.auth.model.PrincipalUser
import com.pokit.category.dto.request.CreateCategoryRequest
import com.pokit.category.dto.request.toDto
import com.pokit.category.dto.response.CategoryCountResponse
import com.pokit.category.dto.response.CategoryResponse
import com.pokit.category.dto.response.toResponse
import com.pokit.category.exception.CategoryErrorCode
import com.pokit.category.model.Category
import com.pokit.category.model.CategoryImage
import com.pokit.category.port.`in`.CategoryUseCase
import com.pokit.common.dto.SliceResponseDto
import com.pokit.common.wrapper.ResponseWrapper.wrapOk
import com.pokit.common.wrapper.ResponseWrapper.wrapSlice
import com.pokit.common.wrapper.ResponseWrapper.wrapUnit
import io.swagger.v3.oas.annotations.Operation
import jakarta.validation.Valid
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/v1/category")
class CategoryController(
private val categoryUseCase: CategoryUseCase
) {
@Operation(summary = "포킷 생성 API")
@ErrorOperation(CategoryErrorCode::class)
@PostMapping
fun createCategory(
@AuthenticationPrincipal user: PrincipalUser,
@Valid @RequestBody request: CreateCategoryRequest
): ResponseEntity<CategoryResponse> =
categoryUseCase.create(request.toDto(), user.id)
.toResponse()
.wrapOk()

@Operation(summary = "포킷 λͺ©λ‘ 쑰회 API")
@GetMapping
fun getCategories(
@AuthenticationPrincipal user: PrincipalUser,
@PageableDefault(
page = 0,
size = 10,
sort = ["createdAt"],
direction = Sort.Direction.DESC
) pageable: Pageable,
@RequestParam(defaultValue = "true") filterUncategorized: Boolean
): ResponseEntity<SliceResponseDto<Category>> =
categoryUseCase.getCategories(user.id, pageable, filterUncategorized)
.wrapSlice()
.wrapOk()

@Operation(summary = "포킷 μˆ˜μ • API")
@ErrorOperation(CategoryErrorCode::class)
@PatchMapping("/{categoryId}")
fun updateCategory(
@AuthenticationPrincipal user: PrincipalUser,
@Valid @RequestBody request: CreateCategoryRequest,
@PathVariable categoryId: Long,
): ResponseEntity<CategoryResponse> =
categoryUseCase.update(request.toDto(), user.id, categoryId)
.toResponse()
.wrapOk()

@Operation(summary = "포킷 μ‚­μ œ API")
@ErrorOperation(CategoryErrorCode::class)
@PutMapping("/{categoryId}")
fun deleteCategory(
@AuthenticationPrincipal user: PrincipalUser,
@PathVariable categoryId: Long,
): ResponseEntity<Unit> =
categoryUseCase.delete(categoryId, user.id)
.wrapUnit()

@Operation(summary = "μœ μ €μ˜ 포킷 개수 쑰회 API")
@GetMapping("/count")
fun getTotalCount(
@AuthenticationPrincipal user: PrincipalUser,
): ResponseEntity<CategoryCountResponse> {
val count = categoryUseCase.getTotalCount(user.id)
return CategoryCountResponse(count)
.wrapOk()
}

@Operation(summary = "포킷 ν”„λ‘œν•„ λͺ©λ‘ 쑰회 API")
@GetMapping("/images")
fun getCategoryImages(): ResponseEntity<List<CategoryImage>> =
categoryUseCase.getAllCategoryImages()
.wrapOk()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.pokit.category.dto.request

import com.pokit.category.dto.CategoryCommand
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

data class CreateCategoryRequest(
@field:NotBlank(message = "포킷 λͺ…은 ν•„μˆ˜ κ°’μž…λ‹ˆλ‹€.")
@field:Size(min = 1, max = 10, message = "μ΅œλŒ€ 10μžκΉŒμ§€ μž…λ ₯ κ°€λŠ₯ν•©λ‹ˆλ‹€.")
val categoryName: String,
val categoryImageId: Int,
)

internal fun CreateCategoryRequest.toDto() = CategoryCommand(
categoryName = this.categoryName,
categoryImageId = this.categoryImageId
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.pokit.category.dto.request

import com.pokit.category.dto.CategoryCommand
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

data class UpdateCategoryRequest (
@field:NotBlank(message = "포킷 λͺ…은 ν•„μˆ˜ κ°’μž…λ‹ˆλ‹€.")
@field:Size(min = 1, max = 10, message = "μ΅œλŒ€ 10μžκΉŒμ§€ μž…λ ₯ κ°€λŠ₯ν•©λ‹ˆλ‹€.")
val categoryName: String,
val categoryImageId: Int,
)

internal fun UpdateCategoryRequest.toDto() = CategoryCommand(
categoryName = this.categoryName,
categoryImageId = this.categoryImageId
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.pokit.category.dto.response

import com.pokit.category.model.Category
import com.pokit.category.model.CategoryImage

data class CategoryResponse(
val categoryId: Long,
var categoryName: String,
var categoryImage: CategoryImage,
)

data class CategoryCountResponse(
val categoryTotalCount: Int,
)

fun Category.toResponse(): CategoryResponse = CategoryResponse(
categoryId = this.categoryId,
categoryName = this.categoryName,
categoryImage = this.categoryImage,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.pokit.common.dto

import org.springframework.data.domain.Slice
import org.springframework.data.domain.Sort

data class SliceResponseDto<T>(
val data: List<T>,
val page: Int?,
val size: Int?,
val sort: Sort,
val hasNext: Boolean,
) {
constructor(slice: Slice<T>) : this(
data = slice.content,
page = slice.pageable.pageNumber,
size = slice.size,
sort = slice.sort,
hasNext = slice.hasNext()
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pokit.common.exception

import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.http.HttpStatus
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
Expand All @@ -8,6 +9,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class ApiExceptionHandler {
private val logger = KotlinLogging.logger { }

private val notValidMessage = "잘λͺ»λœ μž…λ ₯ κ°’μž…λ‹ˆλ‹€."
private val notValidCode = "G_001"

Expand All @@ -16,18 +19,17 @@ class ApiExceptionHandler {
fun handleMethodArgumentNotValidationException(e: MethodArgumentNotValidException): ErrorResponse {
var message = notValidMessage
val allErrors = e.bindingResult.allErrors
if (!allErrors.isEmpty()) {
if (allErrors.isNotEmpty()) {
message = allErrors[0].defaultMessage!!
}

return ErrorResponse(message, notValidCode)
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(NotFoundCustomException::class)
fun handleNotFoundException(e: NotFoundCustomException) = ErrorResponse(e.message!!, e.code)

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ClientValidationException::class)
fun handleClientValidationException(e: ClientValidationException) = ErrorResponse(e.message!!, e.code)
@ExceptionHandler(PokitException::class)
fun handlePokitException(e: PokitException): ErrorResponse {
logger.warn { "PokitException: ${e.message} / $e" }
return ErrorResponse(e.errorCode.message, e.errorCode.code)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pokit.common.wrapper

import com.pokit.common.dto.SliceResponseDto
import org.springframework.data.domain.Slice
import org.springframework.http.ResponseEntity

object ResponseWrapper {
fun <T> T.wrapOk(): ResponseEntity<T> = ResponseEntity.ok(this)

fun <T> Slice<T>.wrapSlice() = SliceResponseDto(this)

fun Unit.wrapUnit(): ResponseEntity<Unit> = ResponseEntity.noContent().build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.pokit.out.persistence.category.impl

import com.pokit.category.model.Category
import com.pokit.category.port.out.CategoryPort
import com.pokit.out.persistence.category.persist.CategoryEntity
import com.pokit.out.persistence.category.persist.CategoryRepository
import com.pokit.out.persistence.category.persist.toDomain
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component

@Component
class CategoryAdapter(
private val categoryRepository: CategoryRepository
) : CategoryPort {
override fun loadAllByUserId(userId: Long, pageable: Pageable): Slice<Category> =
categoryRepository.findByUserIdAndDeleted(userId, false, pageable)
.map { it.toDomain() }

override fun loadByIdAndUserId(id: Long, userId: Long): Category? =
categoryRepository.findByIdAndUserIdAndDeleted(id, userId, false)?.toDomain()

override fun existsByNameAndUserId(name: String, userId: Long): Boolean =
categoryRepository.existsByNameAndUserIdAndDeleted(name, userId, false)

override fun persist(category: Category): Category {
val categoryEntity = CategoryEntity.of(category)
return categoryRepository.save(categoryEntity).toDomain()
}

override fun delete(category: Category) {
categoryRepository.findByIdOrNull(category.categoryId)
?.delete()
}

override fun countByUserId(userId: Long): Int =
categoryRepository.countByUserIdAndDeleted(userId, false)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.pokit.out.persistence.category.impl

import com.pokit.category.model.CategoryImage
import com.pokit.category.port.out.CategoryImagePort
import com.pokit.out.persistence.category.persist.CategoryImageRepository
import com.pokit.out.persistence.category.persist.toDomain
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component

@Component
class CategoryImageAdapter(
private val categoryImageRepository: CategoryImageRepository
) : CategoryImagePort {
override fun loadById(id: Int): CategoryImage? =
categoryImageRepository.findByIdOrNull(id)?.toDomain()

override fun loadAll(): List<CategoryImage> =
categoryImageRepository.findAll()
.map { it.toDomain() }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.pokit.out.persistence.category.persist

import com.pokit.category.model.OpenType
import com.pokit.category.model.Category
import com.pokit.out.persistence.BaseEntity
import jakarta.persistence.*

Expand All @@ -19,14 +19,32 @@ class CategoryEntity(
@Column(name = "name")
var name: String,

@Column(name = "memo")
var memo: String,

@Enumerated(EnumType.STRING)
@Column(name = "open_type")
val openType: OpenType,

@OneToOne
@JoinColumn(name = "image_id")
val image: CategoryImageEntity,
) : BaseEntity()
) : BaseEntity() {

@Column(name = "id_deleted")
var deleted: Boolean = false

fun delete() {
this.deleted = true
}

companion object {
fun of(category: Category) =
CategoryEntity(
id = category.categoryId,
userId = category.userId,
name = category.categoryName,
image = CategoryImageEntity.of(category.categoryImage)
)
}
}

fun CategoryEntity.toDomain() = Category(
categoryId = this.id,
categoryName = this.name,
categoryImage = this.image.toDomain(),
userId = this.userId,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.pokit.out.persistence.category.persist

import com.pokit.category.model.Category
import com.pokit.category.model.CategoryImage
import jakarta.persistence.*

@Table(name = "CATEGORY_IMAGE")
Expand All @@ -8,8 +10,22 @@ class CategoryImageEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
val id: Long = 0L,
val id: Int = 0,

@Column(name = "path")
val path: String,
@Column(name = "url")
val url: String,
) {
companion object {
fun of(categoryImage: CategoryImage) =
CategoryImageEntity(
id = categoryImage.imageId,
url = categoryImage.imageUrl
)
}
}

fun CategoryImageEntity.toDomain() = CategoryImage(
imageId = this.id,
imageUrl = this.url
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.pokit.out.persistence.category.persist

import org.springframework.data.jpa.repository.JpaRepository

interface CategoryImageRepository : JpaRepository<CategoryImageEntity, Int> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.pokit.out.persistence.category.persist

import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.jpa.repository.JpaRepository

interface CategoryRepository : JpaRepository<CategoryEntity, Long> {
fun existsByNameAndUserIdAndDeleted(name: String, userId: Long, deleted: Boolean): Boolean
fun findByUserIdAndDeleted(userId: Long, deleted: Boolean, pageable: Pageable): Slice<CategoryEntity>
fun findByIdAndUserIdAndDeleted(id: Long, userId: Long, deleted: Boolean): CategoryEntity?
fun countByUserIdAndDeleted(userId: Long, deleted: Boolean): Int
}
Loading

0 comments on commit 51cb9d9

Please sign in to comment.