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

feat: News 엔티티 추가 (#2) #6

Merged
merged 10 commits into from
Dec 8, 2023
15 changes: 15 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ dependencies {
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
testImplementation("io.mockk:mockk:1.13.4")
testImplementation("io.mockk:mockk-jvm:1.13.4")
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation("io.kotest:kotest-runner-junit5:5.7.2")
testImplementation("io.kotest:kotest-assertions-core:5.7.2")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3")

// https://mvnrepository.com/artifact/com.github.f4b6a3/ulid-creator
implementation("com.github.f4b6a3:ulid-creator:5.2.2")
}

tasks.withType<KotlinCompile> {
Expand All @@ -63,3 +72,9 @@ tasks.asciidoctor {
inputs.dir(snippetsDir)
dependsOn(tasks.test)
}

allOpen {
annotation("jakarta.persistence.Entity")
annotation ("jakarta.persistence.Embeddable")
annotation ("jakarta.persistence.MappedSuperclass")
}
52 changes: 52 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/common/domain/PrimaryKeyEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package kr.galaxyhub.sc.common.domain

import com.github.f4b6a3.ulid.UlidCreator
import jakarta.persistence.Column
import jakarta.persistence.Id
import jakarta.persistence.MappedSuperclass
import jakarta.persistence.PostLoad
import jakarta.persistence.PostPersist
import java.util.Objects
import java.util.UUID
import org.hibernate.proxy.HibernateProxy
import org.springframework.data.domain.Persistable

@MappedSuperclass
abstract class PrimaryKeyEntity : Persistable<UUID> {

@Id
@Column(columnDefinition = "uuid")
private val id: UUID = UlidCreator.getMonotonicUlid().toUuid()

@Transient
private var _isNew = true

override fun getId(): UUID = id

override fun isNew(): Boolean = _isNew

override fun equals(other: Any?): Boolean {
if (other == null) {
return false
}
if (other !is HibernateProxy && this::class != other::class) {
return false
}
return id == getIdentifier(other)
}

private fun getIdentifier(obj: Any): Any {
return when (obj) {
is HibernateProxy -> obj.hibernateLazyInitializer.identifier
else -> (obj as PrimaryKeyEntity).id
}
}

override fun hashCode(): Int = Objects.hashCode(id)

@PostPersist
@PostLoad
protected fun load() {
_isNew = false
}
}
Comment on lines +14 to +52
Copy link
Contributor Author

Choose a reason for hiding this comment

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

해당 클래스는 식별자를 UUID로 제공하기 위함인데 링크 참고하시면 이해되실 겁니다!

46 changes: 46 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/Content.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package kr.galaxyhub.sc.news.domain

import jakarta.persistence.Column
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.Lob
import jakarta.persistence.ManyToOne

@Entity
class Content(
news: News,
newsInformation: NewsInformation,
language: Language,
content: String,
) {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val sequence: Long? = null

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "news_id", nullable = false)
var news: News = news
protected set

@Embedded
var newsInformation: NewsInformation = newsInformation
protected set

@Enumerated(EnumType.STRING)
@Column(name = "language", nullable = false)
var language: Language = language
protected set

@Lob
@Column(name = "content", nullable = false)
var content: String = content
protected set
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kr.galaxyhub.sc.news.domain

import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter
import java.util.EnumSet

@Converter
class EnumSetLanguageConverter : AttributeConverter<EnumSet<Language>, String> {

override fun convertToDatabaseColumn(attribute: EnumSet<Language>): String {
return attribute.joinToString(separator = SEPARATOR)
}

override fun convertToEntityAttribute(dbData: String?): EnumSet<Language> {
return if (dbData.isNullOrBlank()) {
EnumSet.noneOf(Language::class.java)
} else {
dbData.split(SEPARATOR)
.map { Language.valueOf(it) }
.toCollection(EnumSet.noneOf(Language::class.java))
}
}

companion object {

private const val SEPARATOR = ","
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/Language.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.galaxyhub.sc.news.domain

enum class Language {
ENGLISH,
KOREAN,
}
70 changes: 70 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/News.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package kr.galaxyhub.sc.news.domain

import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Convert
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.FetchType
import jakarta.persistence.OneToMany
import java.time.ZonedDateTime
import java.util.EnumSet
import kr.galaxyhub.sc.common.domain.PrimaryKeyEntity

@Entity
class News(
newsType: NewsType,
originId: Long,
originUrl: String,
publishedAt: ZonedDateTime,
) : PrimaryKeyEntity() {
Copy link
Member

Choose a reason for hiding this comment

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

원문의 URL 를 담을 수 있는 컬럼이 있다면 좋을 것 같습니다.

Copy link
Member

Choose a reason for hiding this comment

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

원문 링크를 함께 제공하여 다음과 같은 효과를 얻을 수 있습니다.

  1. 원문 링크를 함께 게시하여 출처를 명시할 수 있습니다.

  2. 모든 소식의 내용을 모두 번역하여 보여주면 좋겠지만 Iea 2953 와 같은 대규모 프로모션 컨텐츠는 개발에 많은 시간이 소요될 것 입니다. 이런 경우 요약본만 번역하고 원문 링크를 게시하여 원문을 보도록 안내할 수 있습니다.

Copy link
Member

Choose a reason for hiding this comment

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

한번 검토해주시면 감사드립니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

originUrl 이라는 컬럼을 추가하면 되겠네요!


@Enumerated(EnumType.STRING)
@Column(name = "news_type", nullable = false)
var newsType: NewsType = newsType
protected set

@Column(name = "origin_id", nullable = false)
var originId: Long = originId
protected set

@Column(name = "originUrl", nullable = false)
var originUrl: String = originUrl
protected set

@Column(name = "published_at", nullable = false)
var publishedAt: ZonedDateTime = publishedAt
protected set

@Embedded
var newsInformation: NewsInformation = NewsInformation.EMPTY
protected set

@Convert(converter = EnumSetLanguageConverter::class)
@Column(name = "support_languages", nullable = false)
private val mutableSupportLanguages: EnumSet<Language> = EnumSet.noneOf(Language::class.java)
val supportLanguages: Set<Language> get() = mutableSupportLanguages.toHashSet()

@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], mappedBy = "news")
private val contents: MutableList<Content> = mutableListOf()

fun addContent(content: Content) {
validateAddContent(content)
if (mutableSupportLanguages.isEmpty()) {
newsInformation = content.newsInformation
}
contents.add(content)
mutableSupportLanguages.add(content.language)
}

private fun validateAddContent(content: Content) {
if (content.news != this) {
throw IllegalArgumentException("컨텐츠에 등록된 뉴스가 동일하지 않습니다.") // TODO 명확한 예외 정의할 것
}
if (mutableSupportLanguages.contains(content.language)) {
throw IllegalArgumentException("이미 해당 언어로 작성된 뉴스가 있습니다.") // TODO 명확한 예외 정의할 것
}
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/NewsInformation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kr.galaxyhub.sc.news.domain

import jakarta.persistence.Column
import jakarta.persistence.Embeddable

@Embeddable
data class NewsInformation(
@Column(name = "title", nullable = true)
val title: String?,

@Column(name = "excerpt", nullable = true)
val excerpt: String?
) {
companion object {

val EMPTY = NewsInformation(null, null)
}
}

6 changes: 6 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/NewsType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.galaxyhub.sc.news.domain

enum class NewsType {
PATCH_NOTE,
NEWS,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package kr.galaxyhub.sc.news.domain

import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.inspectors.forAll
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import java.util.EnumSet

class EnumSetLanguageConverterTest : DescribeSpec({

val enumSetLanguageConverter = EnumSetLanguageConverter()

describe("convertToDatabaseColumn") {
context("요소가 한 개 이면") {
val attribute = EnumSet.of(Language.ENGLISH)

it(",가 없는 문자열을 반환한다.") {
enumSetLanguageConverter.convertToDatabaseColumn(attribute) shouldBe "ENGLISH"
}
}
Comment on lines +16 to +23
Copy link
Contributor Author

Choose a reason for hiding this comment

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

해당 프로젝트에서 Describe 패턴을 사용하여 테스트를 작성해볼까 합니다.
Describe 패턴의 설명은 링크 참조하시면 됩니다.
또한, Kotest를 사용하여 테스트 코드를 작성했는데 사용법은 링크 참조해주시면 될 것 같습니다.


context("요소가 여러 개 이면") {
val attribute = EnumSet.of(Language.ENGLISH, Language.KOREAN)

it(",가 있는 문자열을 반환한다.") {
val expect = enumSetLanguageConverter.convertToDatabaseColumn(attribute)
assertSoftly {
expect shouldContain ","
expect shouldContain "ENGLISH"
expect shouldContain "KOREAN"
}
}
}

context("요소가 없으면") {
val attribute = EnumSet.noneOf(Language::class.java)

it("빈 문자열을 반환한다.") {
enumSetLanguageConverter.convertToDatabaseColumn(attribute) shouldBe ""
}
}
}

describe("convertToEntityAttribute") {
context(",가 없는 문자열이면") {
val dbData = "ENGLISH"

it("한 개의 요소를 반환한다.") {
val expect = enumSetLanguageConverter.convertToEntityAttribute(dbData)

assertSoftly {
expect shouldHaveSize 1
expect shouldContainExactly setOf(Language.ENGLISH)
}
}
}

context(",가 있는 문자열이면") {
val dbData = "ENGLISH,KOREAN"

it("여러 개의 요소를 반환한다.") {
val expect = enumSetLanguageConverter.convertToEntityAttribute(dbData)

assertSoftly {
expect shouldHaveSize 2
expect shouldContainExactly setOf(Language.ENGLISH, Language.KOREAN)
}
}
}

context("null 또는 빈 문자열이면") {
val dbData = listOf(null, "", " ", "\t", "\n")

it("비어 있는 요소를 반환한다.") {
dbData.forAll {
enumSetLanguageConverter.convertToEntityAttribute(it) shouldHaveSize 0
}
}
}
}
})
Loading