Skip to content

Commit

Permalink
[feature] Category Domain 정의 및 CRUD 개발 (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
dojinyou authored and k-kbk committed Sep 29, 2023
1 parent e0f9cc9 commit 9505601
Show file tree
Hide file tree
Showing 20 changed files with 678 additions and 26 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ val javaVersion = "${property("javaVersion")}"
val testContainerVersion = "${property("testContainerVersion")}"
val restdocsApiVersion = "${property("restdocsApiVersion")}"
val springMockkVersion = "${property("springMockkVersion")}"
val autoParamsVersion = "${property("autoParamsVersion")}"

java {
sourceCompatibility = JavaVersion.valueOf("VERSION_$javaVersion")
Expand Down Expand Up @@ -60,6 +61,7 @@ dependencies {
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
testImplementation("com.epages:restdocs-api-spec-mockmvc:$restdocsApiVersion")
testImplementation("com.ninja-squad:springmockk:$springMockkVersion")
testImplementation("io.github.autoparams:autoparams-kotlin:$autoParamsVersion")
}

tasks.withType<KotlinCompile> {
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ springDependencyManagementVersion=1.1.3
testContainerVersion=1.19.0
restdocsApiVersion=0.18.2
springMockkVersion=4.0.2
autoParamsVersion=3.1.0
17 changes: 16 additions & 1 deletion src/main/kotlin/com/mjucow/eatda/domain/common/BaseEntity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import java.time.ZoneId
abstract class BaseEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
val id: Long = DEFAULT_ID,
) {
@Column(nullable = false, updatable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZONE_ID)
Expand All @@ -38,7 +38,22 @@ abstract class BaseEntity(
updatedAt = LocalDateTime.now(ZONE_ID)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as BaseEntity
if (id == DEFAULT_ID || other.id == DEFAULT_ID) return false

return id == other.id
}

override fun hashCode(): Int {
return id.hashCode()
}

companion object {
const val DEFAULT_ID = 0L
val ZONE_ID: ZoneId = ZoneId.of("Asia/Seoul")
}
}
16 changes: 0 additions & 16 deletions src/main/kotlin/com/mjucow/eatda/domain/store/category/Category.kt

This file was deleted.

29 changes: 29 additions & 0 deletions src/main/kotlin/com/mjucow/eatda/domain/store/entity/Category.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.mjucow.eatda.domain.store.entity

import com.mjucow.eatda.domain.common.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Table

@Entity
@Table(name = "category")
class Category() : BaseEntity() {
constructor(name: String) : this() {
validateName(name)
this.name = name
}

@Column(nullable = false, unique = true)
var name: String = ""
set(value) {
validateName(value)
field = value
}

private fun validateName(name: String) {
require(name.isNotBlank() && name.trim().length <= MAX_NAME_LENGTH)
}
companion object {
const val MAX_NAME_LENGTH = 31
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.mjucow.eatda.domain.store.service.command

import com.mjucow.eatda.domain.store.entity.Category
import com.mjucow.eatda.domain.store.service.command.dto.CreateCommand
import com.mjucow.eatda.domain.store.service.command.dto.UpdateNameCommand
import com.mjucow.eatda.persistence.store.CategoryRepository
import jakarta.transaction.Transactional
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service

@Service
@Transactional
class CategoryCommandService(
private val repository: CategoryRepository,
) {
fun create(command: CreateCommand): Long {
val newName = command.name.trim()
require(repository.existsByName(newName).not())

return repository.save(Category(newName)).id
}

fun updateName(id: Long, command: UpdateNameCommand) {
val domain = getById(id)

val newName = command.name.trim()
if (domain.name == newName) {
return
}
domain.name = newName

repository.save(domain)
}

fun delete(id: Long) {
val domain = repository.findByIdOrNull(id) ?: return
repository.delete(domain)
}

private fun getById(id: Long): Category {
return repository.findByIdOrNull(id) ?: throw IllegalArgumentException()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.mjucow.eatda.domain.store.service.command.dto

data class CreateCommand(val name: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.mjucow.eatda.domain.store.service.command.dto

data class UpdateNameCommand(val name: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mjucow.eatda.domain.store.service.query

import com.mjucow.eatda.domain.store.service.query.dto.Categories
import com.mjucow.eatda.domain.store.service.query.dto.CategoryDto
import com.mjucow.eatda.persistence.store.CategoryRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service

@Service
class CategoryQueryService(
val repository: CategoryRepository,
) {
fun findAll(): Categories {
return Categories(repository.findAll().map(CategoryDto::from))
}

fun findById(id: Long): CategoryDto? {
return repository.findByIdOrNull(id)?.run { CategoryDto.from(this) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mjucow.eatda.domain.store.service.query.dto

data class Categories(
val categoryList: List<CategoryDto>,
) : ArrayList<CategoryDto>(categoryList)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.mjucow.eatda.domain.store.service.query.dto

import com.mjucow.eatda.domain.store.entity.Category

data class CategoryDto(
val id: Long,
val name: String,
) {
companion object {
fun from(domain: Category): CategoryDto {
return CategoryDto(domain.id, domain.name)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mjucow.eatda.persistence.store

import com.mjucow.eatda.domain.store.entity.Category
import org.springframework.data.jpa.repository.JpaRepository

interface CategoryRepository : JpaRepository<Category, Long> {
fun existsByName(name: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.mjucow.eatda.presentation.store.category

import com.mjucow.eatda.domain.store.service.command.CategoryCommandService
import com.mjucow.eatda.domain.store.service.command.dto.CreateCommand
import com.mjucow.eatda.domain.store.service.command.dto.UpdateNameCommand
import com.mjucow.eatda.domain.store.service.query.CategoryQueryService
import com.mjucow.eatda.domain.store.service.query.dto.Categories
import com.mjucow.eatda.domain.store.service.query.dto.CategoryDto
import com.mjucow.eatda.presentation.common.ApiResponse
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.DeleteMapping
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 org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1/categories")
class CategoryController(
val categoryQueryService: CategoryQueryService,
val categoryCommandService: CategoryCommandService,
) {
@GetMapping
@ResponseStatus(HttpStatus.OK)
fun findAll(): ApiResponse<Categories> {
return ApiResponse.success(categoryQueryService.findAll())
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun create(@RequestBody command: CreateCommand): ApiResponse<Long> {
val id = categoryCommandService.create(command)
return ApiResponse.success(id)
}

@GetMapping("/{categoryId}")
@ResponseStatus(HttpStatus.OK)
fun findById(@PathVariable("categoryId") id: Long): ApiResponse<CategoryDto> {
return ApiResponse.success(categoryQueryService.findById(id))
}

@DeleteMapping("/{categoryId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteById(@PathVariable("categoryId") id: Long) {
categoryCommandService.delete(id)
}

@PatchMapping("/{categoryId}")
@ResponseStatus(HttpStatus.OK)
fun updateNameById(
@PathVariable("categoryId") id: Long,
@RequestBody command: UpdateNameCommand,
) {
categoryCommandService.updateName(id, command)
}
}
13 changes: 7 additions & 6 deletions src/main/resources/db/changelog/230925-init.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
-- liquibase formatted sql

-- changeset liquibase:1
CREATE TABLE Category (
id bigint NOT NULL,
name varchar(31) NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
CREATE TABLE category (
id bigserial NOT NULL,
name varchar(31) NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL
);

ALTER TABLE Category ADD CONSTRAINT PK_CATEGORY PRIMARY KEY (id);
ALTER TABLE category ADD CONSTRAINT PK_CATEGORY PRIMARY KEY (id);
CREATE UNIQUE INDEX idx_category_name ON category(name);
24 changes: 24 additions & 0 deletions src/test/kotlin/com/mjucow/eatda/domain/AbstractDataTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.mjucow.eatda.domain

import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.test.context.ActiveProfiles
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
abstract class AbstractDataTest {
companion object {
@Container
@ServiceConnection
val POSTGRESQL_CONTAINER = PostgreSQLContainer("postgres:16.0-alpine")
.withUsername("test-user")
.withPassword("test-password")
.withDatabaseName("test_db")!!
}
}
Loading

0 comments on commit 9505601

Please sign in to comment.