Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a4a6b3355 | ||
|
|
59cf934058 | ||
|
|
02553cdec7 | ||
|
|
7d3b445303 | ||
|
|
c7f6e3ca81 | ||
|
|
9ff1c6ecec | ||
|
|
022e344fea | ||
|
|
a2b580e8e7 | ||
|
|
36c34a77c4 | ||
|
|
4e2cc2ea10 | ||
|
|
122766f76e | ||
|
|
6ed8a0e62a | ||
|
|
b399db7604 | ||
|
|
78e8f9b7ec | ||
|
|
c675c10417 | ||
|
|
ec07b2c637 | ||
|
|
d3eb581d94 |
@@ -1,8 +1,5 @@
|
||||
package me.jiniworld.demohx.application.notice.domain
|
||||
|
||||
import me.jiniworld.demohx.DateTimeUtils
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeSimple
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class Notice(
|
||||
@@ -10,10 +7,4 @@ data class Notice(
|
||||
val title: String,
|
||||
val content: String,
|
||||
val createdAt: LocalDateTime,
|
||||
) {
|
||||
fun mapToNoticeSimple() =
|
||||
NoticeSimple(id = id, title = title, createdOn = DateTimeUtils.toDateString(createdAt))
|
||||
|
||||
fun mapToNoticeDetail() =
|
||||
NoticeDetail(id = id, title = title, content = content, createdAt = DateTimeUtils.toString(createdAt))
|
||||
}
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.jiniworld.demohx.application.notice.port.output
|
||||
package me.jiniworld.demohx.application.notice.domain
|
||||
|
||||
data class NoticeDetail(
|
||||
val id: Long,
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.jiniworld.demohx.application.notice.port.output
|
||||
package me.jiniworld.demohx.application.notice.domain
|
||||
|
||||
data class NoticeSimple(
|
||||
val id: Long,
|
||||
@@ -1,7 +1,7 @@
|
||||
package me.jiniworld.demohx.application.notice.port.input
|
||||
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeSimple
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeSimple
|
||||
|
||||
interface GetNoticeQuery {
|
||||
fun getNoticeSimples(command: GetNoticesCommand): List<NoticeSimple>?
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package me.jiniworld.demohx.application.notice.port.output
|
||||
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeSimple
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface LoadNoticePort {
|
||||
fun loadNotices(pageable: Pageable): List<Notice>?
|
||||
fun loadNotice(id: Long): Notice?
|
||||
fun loadNotices(pageable: Pageable): List<NoticeSimple>?
|
||||
fun loadNotice(id: Long): NoticeDetail?
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package me.jiniworld.demohx.application.notice.service
|
||||
|
||||
import me.jiniworld.demohx.annotation.UseCase
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticeQuery
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticesCommand
|
||||
import me.jiniworld.demohx.application.notice.port.output.LoadNoticePort
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeDetail
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@@ -17,8 +17,7 @@ internal class GetNoticeService(
|
||||
|
||||
override fun getNoticeSimples(command: GetNoticesCommand) =
|
||||
loadNoticePort.loadNotices(PageRequest.of(command.page, command.size, Sort.by(Sort.Order.desc("id"))))
|
||||
?.map { it.mapToNoticeSimple() }
|
||||
|
||||
override fun getNoticeDetail(id: Long): NoticeDetail? =
|
||||
loadNoticePort.loadNotice(id)?.mapToNoticeDetail()
|
||||
loadNoticePort.loadNotice(id)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
implementation("org.springframework.data:spring-data-commons")
|
||||
implementation("org.springframework:spring-tx")
|
||||
implementation("org.hibernate.validator:hibernate-validator:8.0.0.Final")
|
||||
|
||||
|
||||
implementation(project(":util:common-util"))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package me.jiniworld.demohx.application.item.domain
|
||||
|
||||
data class Item(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val price: Int,
|
||||
val stock: Int,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package me.jiniworld.demohx.application.item.port.input
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.jiniworld.demohx.application.item.domain.Item
|
||||
|
||||
|
||||
interface GetItemQuery {
|
||||
fun getItems(command: GetItemsCommand): Flow<Item>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package me.jiniworld.demohx.application.item.port.input
|
||||
|
||||
data class GetItemsCommand(
|
||||
val page: Int,
|
||||
val size: Int,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.jiniworld.demohx.application.item.port.output
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.jiniworld.demohx.application.item.domain.Item
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface LoadItemPort {
|
||||
suspend fun loadItem(id: String): Item?
|
||||
fun loadItems(pageable: Pageable): Flow<Item>
|
||||
fun loadItems(ids: Collection<String>): Flow<Item>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package me.jiniworld.demohx.application.item.service
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.jiniworld.demohx.annotation.UseCase
|
||||
import me.jiniworld.demohx.application.item.domain.Item
|
||||
import me.jiniworld.demohx.application.item.port.input.GetItemQuery
|
||||
import me.jiniworld.demohx.application.item.port.input.GetItemsCommand
|
||||
import me.jiniworld.demohx.application.item.port.output.LoadItemPort
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@UseCase
|
||||
internal class GetItemService(
|
||||
private val loadItemPort: LoadItemPort,
|
||||
): GetItemQuery {
|
||||
|
||||
override fun getItems(command: GetItemsCommand): Flow<Item> {
|
||||
return loadItemPort.loadItems(
|
||||
PageRequest.of(command.page, command.size, Sort.by(
|
||||
Sort.Order.desc("id")))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
package me.jiniworld.demohx.application.notice.domain
|
||||
|
||||
import me.jiniworld.demohx.DateTimeUtils
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeSimple
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class Notice(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val summary: Summary,
|
||||
val content: String,
|
||||
val createdAt: LocalDateTime,
|
||||
) {
|
||||
fun mapToNoticeSimple() =
|
||||
NoticeSimple(id = id, title = title, createdOn = DateTimeUtils.toDateString(createdAt))
|
||||
data class Summary(
|
||||
val id: String? = null,
|
||||
val title: String,
|
||||
val createdAt: String? = null,
|
||||
)
|
||||
|
||||
fun mapToNoticeDetail() =
|
||||
NoticeDetail(id = id, title = title, content = content, createdAt = DateTimeUtils.toString(createdAt))
|
||||
data class Detail(
|
||||
val id: String? = null,
|
||||
val title: String,
|
||||
val content: String,
|
||||
val createdAt: String? = null,
|
||||
)
|
||||
|
||||
fun detail(): Detail {
|
||||
return Detail(id = summary.id, title = summary.title, content = content, createdAt = summary.createdAt)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package me.jiniworld.demohx.application.notice.port.input
|
||||
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeSimple
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
|
||||
interface GetNoticeQuery {
|
||||
suspend fun getNoticeSimples(command: GetNoticesCommand): List<NoticeSimple>?
|
||||
suspend fun getNoticeDetail(id: String): NoticeDetail?
|
||||
fun getNoticeSummaries(command: GetNoticesCommand): Flow<Notice.Summary>
|
||||
suspend fun getNoticeDetail(id: String): Notice.Detail?
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package me.jiniworld.demohx.application.notice.port.input
|
||||
|
||||
import javax.validation.constraints.Min
|
||||
|
||||
data class GetNoticesCommand(
|
||||
val page: Int,
|
||||
val size: Int,
|
||||
@Min(0) val page: Int,
|
||||
@Min(5) val size: Int,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package me.jiniworld.demohx.application.notice.port.input
|
||||
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class RegisterNoticeCommand(
|
||||
@NotBlank val title: String,
|
||||
@NotBlank val content: String,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package me.jiniworld.demohx.application.notice.port.input
|
||||
|
||||
interface RegisterNoticeUseCase {
|
||||
suspend fun registerNotice(command: RegisterNoticeCommand)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package me.jiniworld.demohx.application.notice.port.output
|
||||
|
||||
data class NoticeDetail(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val content: String,
|
||||
val createdAt: String,
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
package me.jiniworld.demohx.application.notice.port.output
|
||||
|
||||
data class NoticeSimple(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val createdOn: String,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.jiniworld.demohx.application.notice.port.output
|
||||
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
|
||||
interface SaveNoticePort {
|
||||
suspend fun saveNotice(notice: Notice)
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package me.jiniworld.demohx.application.notice.service
|
||||
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import me.jiniworld.demohx.annotation.UseCase
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticeQuery
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticesCommand
|
||||
import me.jiniworld.demohx.application.notice.port.output.LoadNoticePort
|
||||
import me.jiniworld.demohx.application.notice.port.output.NoticeDetail
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@@ -17,12 +16,10 @@ internal class GetNoticeService(
|
||||
private val loadNoticePort: LoadNoticePort,
|
||||
) : GetNoticeQuery {
|
||||
|
||||
override suspend fun getNoticeSimples(command: GetNoticesCommand) =
|
||||
override fun getNoticeSummaries(command: GetNoticesCommand) =
|
||||
loadNoticePort.loadNotices(PageRequest.of(command.page, command.size, Sort.by(
|
||||
Sort.Order.desc("id"))))
|
||||
.map { it.mapToNoticeSimple() }
|
||||
.toList()
|
||||
Sort.Order.desc("id")))).map { it.summary }
|
||||
|
||||
override suspend fun getNoticeDetail(id: String): NoticeDetail? =
|
||||
loadNoticePort.loadNotice(id)?.mapToNoticeDetail()
|
||||
override suspend fun getNoticeDetail(id: String): Notice.Detail? =
|
||||
loadNoticePort.loadNotice(id)?.detail()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package me.jiniworld.demohx.application.notice.service
|
||||
|
||||
import me.jiniworld.demohx.annotation.UseCase
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import me.jiniworld.demohx.application.notice.port.input.RegisterNoticeCommand
|
||||
import me.jiniworld.demohx.application.notice.port.input.RegisterNoticeUseCase
|
||||
import me.jiniworld.demohx.application.notice.port.output.SaveNoticePort
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@UseCase
|
||||
internal class RegisterNoticeService(
|
||||
private val saveNoticePort: SaveNoticePort,
|
||||
): RegisterNoticeUseCase {
|
||||
|
||||
@Transactional
|
||||
override suspend fun registerNotice(command: RegisterNoticeCommand) {
|
||||
saveNoticePort.saveNotice(Notice(Notice.Summary(title = command.title), content = command.content))
|
||||
}
|
||||
|
||||
}
|
||||
11
infrastructure/datastore-mariadb-reactive/build.gradle.kts
Normal file
11
infrastructure/datastore-mariadb-reactive/build.gradle.kts
Normal file
@@ -0,0 +1,11 @@
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
|
||||
runtimeOnly("org.mariadb:r2dbc-mariadb:1.1.3")
|
||||
|
||||
implementation(project(":util:common-util"))
|
||||
implementation(project(":core:demo-reactive-core"))
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Table("notice")
|
||||
internal class NoticeEntity {
|
||||
@Id
|
||||
var id: String = ""
|
||||
|
||||
var title: String = ""
|
||||
|
||||
var content: String = ""
|
||||
|
||||
@CreatedDate
|
||||
var createdAt: LocalDateTime = LocalDateTime.now()
|
||||
|
||||
@LastModifiedDate
|
||||
var updatedAt: LocalDateTime? = null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import me.jiniworld.demohx.DateTimeUtils
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
|
||||
internal object NoticeMapper {
|
||||
|
||||
fun mapToNotice(entity: NoticeEntity) =
|
||||
Notice(summary = Notice.Summary(id = entity.id, title = entity.title,
|
||||
createdAt = DateTimeUtils.toString(entity.createdAt)), content = entity.content)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.jiniworld.demohx.annotation.PersistenceAdapter
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import me.jiniworld.demohx.application.notice.port.output.LoadNoticePort
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
@PersistenceAdapter
|
||||
internal class NoticePersistenceAdapter(
|
||||
private val noticeRepository: NoticeRepository,
|
||||
) : LoadNoticePort {
|
||||
override fun loadNotices(pageable: Pageable): Flow<Notice> {
|
||||
return noticeRepository.findAllBy(pageable).map { NoticeMapper.mapToNotice(it) }
|
||||
}
|
||||
|
||||
override suspend fun loadNotice(id: String): Notice? {
|
||||
return noticeRepository.findById(id)?.let { NoticeMapper.mapToNotice(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
internal interface NoticeRepository: CoroutineCrudRepository<NoticeEntity, String> {
|
||||
fun findAllBy(pageable: Pageable): Flow<NoticeEntity>
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import me.jiniworld.demohx.DateTimeUtils
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeSimple
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import java.time.LocalDateTime
|
||||
@@ -28,4 +31,11 @@ internal class NoticeEntity {
|
||||
|
||||
fun mapToNotice() =
|
||||
Notice(id = id, title = title, content = content, createdAt = createdAt)
|
||||
|
||||
fun mapToNoticeSimple() =
|
||||
NoticeSimple(id = id, title = title, createdOn = DateTimeUtils.toDateString(createdAt))
|
||||
|
||||
fun mapToNoticeDetail() =
|
||||
NoticeDetail(id = id, title = title, content = content, createdAt = DateTimeUtils.toString(createdAt))
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import me.jiniworld.demohx.annotation.PersistenceAdapter
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeDetail
|
||||
import me.jiniworld.demohx.application.notice.domain.NoticeSimple
|
||||
import me.jiniworld.demohx.application.notice.port.output.LoadNoticePort
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
@@ -10,11 +11,11 @@ import org.springframework.data.repository.findByIdOrNull
|
||||
internal class NoticePersistenceAdapter(
|
||||
private val noticeRepository: NoticeRepository,
|
||||
) : LoadNoticePort {
|
||||
override fun loadNotices(pageable: Pageable): List<Notice>? {
|
||||
return noticeRepository.findAllBy(pageable).map { it.mapToNotice() }.toList()
|
||||
override fun loadNotices(pageable: Pageable): List<NoticeSimple>? {
|
||||
return noticeRepository.findAllBy(pageable).map { it.mapToNoticeSimple() }.toList()
|
||||
}
|
||||
|
||||
override fun loadNotice(id: Long): Notice? {
|
||||
return noticeRepository.findByIdOrNull(id)?.mapToNotice()
|
||||
override fun loadNotice(id: Long): NoticeDetail? {
|
||||
return noticeRepository.findByIdOrNull(id)?.mapToNoticeDetail()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package me.jiniworld.demohx.persistence.item
|
||||
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Document("item")
|
||||
internal class ItemDocument(val name: String, val price: Int, var stock: Int) {
|
||||
@Id
|
||||
var id: String = ""
|
||||
|
||||
@CreatedDate
|
||||
var createdAt: LocalDateTime = LocalDateTime.now()
|
||||
|
||||
@LastModifiedDate
|
||||
var updatedAt: LocalDateTime? = null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.jiniworld.demohx.persistence.item
|
||||
|
||||
import me.jiniworld.demohx.application.item.domain.Item
|
||||
|
||||
internal object ItemMapper {
|
||||
|
||||
fun mapToDomainEntity(doc: ItemDocument): Item {
|
||||
return Item(doc.id, doc.name, doc.price, doc.stock)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package me.jiniworld.demohx.persistence.item
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.jiniworld.demohx.annotation.PersistenceAdapter
|
||||
import me.jiniworld.demohx.application.item.domain.Item
|
||||
import me.jiniworld.demohx.application.item.port.output.LoadItemPort
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
@PersistenceAdapter
|
||||
internal class ItemPersistenceAdapter(
|
||||
private val itemRepository: ItemRepository,
|
||||
): LoadItemPort {
|
||||
|
||||
override suspend fun loadItem(id: String): Item? {
|
||||
return itemRepository.findById(id)
|
||||
?.let { ItemMapper.mapToDomainEntity(it) }
|
||||
}
|
||||
|
||||
override fun loadItems(pageable: Pageable): Flow<Item> {
|
||||
return itemRepository.findAllBy(pageable)
|
||||
.map { ItemMapper.mapToDomainEntity(it) }
|
||||
}
|
||||
|
||||
override fun loadItems(ids: Collection<String>): Flow<Item> {
|
||||
return itemRepository.findAllById(ids)
|
||||
.map { ItemMapper.mapToDomainEntity(it) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.jiniworld.demohx.persistence.item
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
internal interface ItemRepository: CoroutineCrudRepository<ItemDocument, String> {
|
||||
fun findAllBy(pageable: Pageable): Flow<ItemDocument>
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.annotation.LastModifiedDate
|
||||
@@ -8,13 +7,9 @@ import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Document(value = "notice")
|
||||
internal class NoticeDocument {
|
||||
internal class NoticeDocument(var title: String, var content: String) {
|
||||
@Id
|
||||
var id: String = ""
|
||||
|
||||
var title: String = ""
|
||||
|
||||
var content: String = ""
|
||||
var id: String? = null
|
||||
|
||||
@CreatedDate
|
||||
var createdAt: LocalDateTime = LocalDateTime.now()
|
||||
@@ -22,6 +17,4 @@ internal class NoticeDocument {
|
||||
@LastModifiedDate
|
||||
var updatedAt: LocalDateTime? = null
|
||||
|
||||
fun mapToNotice() =
|
||||
Notice(id = id, title = title, content = content, createdAt = createdAt)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package me.jiniworld.demohx.persistence.notice
|
||||
|
||||
import me.jiniworld.demohx.DateTimeUtils
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
|
||||
internal object NoticeMapper {
|
||||
|
||||
fun mapToNotice(doc: NoticeDocument) =
|
||||
Notice(summary = Notice.Summary(id = doc.id, title = doc.title,
|
||||
createdAt = DateTimeUtils.toString(doc.createdAt)), content = doc.content)
|
||||
|
||||
fun mapToDocument(notice: Notice) =
|
||||
NoticeDocument(title = notice.summary.title, content = notice.content)
|
||||
|
||||
}
|
||||
@@ -5,17 +5,22 @@ import kotlinx.coroutines.flow.map
|
||||
import me.jiniworld.demohx.annotation.PersistenceAdapter
|
||||
import me.jiniworld.demohx.application.notice.domain.Notice
|
||||
import me.jiniworld.demohx.application.notice.port.output.LoadNoticePort
|
||||
import me.jiniworld.demohx.application.notice.port.output.SaveNoticePort
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
@PersistenceAdapter
|
||||
internal class NoticePersistenceAdapter(
|
||||
private val noticeRepository: NoticeRepository,
|
||||
) : LoadNoticePort {
|
||||
) : LoadNoticePort, SaveNoticePort {
|
||||
override fun loadNotices(pageable: Pageable): Flow<Notice> {
|
||||
return noticeRepository.findAllBy(pageable).map { it.mapToNotice() }
|
||||
return noticeRepository.findAllBy(pageable).map { NoticeMapper.mapToNotice(it) }
|
||||
}
|
||||
|
||||
override suspend fun loadNotice(id: String): Notice? {
|
||||
return noticeRepository.findById(id)?.mapToNotice()
|
||||
return noticeRepository.findById(id)?.let { NoticeMapper.mapToNotice(it) }
|
||||
}
|
||||
|
||||
override suspend fun saveNotice(notice: Notice) {
|
||||
noticeRepository.save(NoticeMapper.mapToDocument(notice))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package me.jiniworld.demohx.notice.adapter.input.web
|
||||
|
||||
import io.mockk.mockk
|
||||
import me.jiniworld.demohx.notice.application.port.input.GetNoticeQuery
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||
|
||||
@AutoConfigureRestDocs
|
||||
@AutoConfigureMockMvc
|
||||
internal class GetNoticeControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
private lateinit var getNoticeController: GetNoticeController
|
||||
private lateinit var getNoticeQuery: GetNoticeQuery
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
val registerNoticeUseCase = mockk<GetNoticeQuery>(relaxed = true)
|
||||
this.getNoticeQuery = registerNoticeUseCase
|
||||
val cont = GetNoticeController(registerNoticeUseCase)
|
||||
this.getNoticeController = cont
|
||||
this.mockMvc = MockMvcBuilders.standaloneSetup(cont).build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getNotices() {
|
||||
val resultActions = mockMvc.perform(
|
||||
MockMvcRequestBuilders.get("/v1/notices")
|
||||
.header("Content-Type", "application/json")
|
||||
)
|
||||
|
||||
// then
|
||||
resultActions
|
||||
.andExpect(MockMvcResultMatchers.status().isOk)
|
||||
.andExpect(MockMvcResultMatchers.jsonPath("$.result", Matchers.`is`("success")))
|
||||
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andDo(MockMvcResultHandlers.print())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getNotice() {
|
||||
val resultActions = mockMvc.perform(
|
||||
MockMvcRequestBuilders.get("/v1/notices/{id}", 1)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
)
|
||||
|
||||
// then
|
||||
resultActions
|
||||
.andExpect(MockMvcResultMatchers.status().isOk)
|
||||
.andExpect(MockMvcResultMatchers.jsonPath("$.result", Matchers.`is`("success")))
|
||||
.andExpect(MockMvcResultMatchers.jsonPath("$.data").exists())
|
||||
.andExpect(MockMvcResultMatchers.jsonPath("$.data.id").isNotEmpty)
|
||||
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andDo(MockMvcResultHandlers.print())
|
||||
//
|
||||
// resultActions.andDo(document("notice-search/success",
|
||||
// requestFields(
|
||||
// fieldWithPath("id").description("notice id")
|
||||
// )
|
||||
// ));
|
||||
// prepa
|
||||
// resource(
|
||||
// ResourceSnippetParameters.builder()
|
||||
// .description("v1 test")
|
||||
// .pathParameters(
|
||||
// parameterWithName("name").description("이름"))
|
||||
// .responseFields(
|
||||
// fieldWithPath("data").description("입력한 이름")
|
||||
//// fieldWithPath("products").description("The product line item of the cart."),
|
||||
//// subsectionWithPath("products[]._links.product").description("Link to the product."),
|
||||
//// fieldWithPath("products[].quantity").description("The quantity of the line item."),
|
||||
//// subsectionWithPath("products[].product").description("The product the line item relates to."),
|
||||
//// subsectionWithPath("_links").description("Links section.")
|
||||
// )
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test1() {
|
||||
val resultActions = mockMvc.perform(
|
||||
MockMvcRequestBuilders.get("/v1/notices/test")
|
||||
.header("Content-Type", "application/json")
|
||||
)
|
||||
|
||||
// then
|
||||
resultActions
|
||||
.andExpect(MockMvcResultMatchers.status().isOk)
|
||||
.andExpect(MockMvcResultMatchers.jsonPath("$.result", Matchers.`is`("success")))
|
||||
.andExpect(MockMvcResultMatchers.jsonPath("$.data", Matchers.`is`("test")))
|
||||
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andDo(MockMvcResultHandlers.print())
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import me.jiniworld.demohx.annotation.WebAdapter
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticeQuery
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticesCommand
|
||||
import me.jiniworld.demohx.model.DataResponse
|
||||
import me.jiniworld.demohx.model.NotFoundException
|
||||
import org.springframework.validation.annotation.Validated
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@Validated
|
||||
@WebAdapter
|
||||
@Tag(name = "setting-system", description = "설정-시스템(공지사항, FAQ, 이용약관, 메타정보 등)")
|
||||
@RestController
|
||||
@@ -23,18 +24,16 @@ internal class GetNoticeController(
|
||||
@RequestParam(value = "page", required = false, defaultValue = "0") page: Int,
|
||||
@RequestParam(value = "size", required = false, defaultValue = "10") size: Int,
|
||||
) = getNoticeQuery.getNoticeSimples(GetNoticesCommand(page = page, size = size))
|
||||
?.let { DataResponse(data = it) }
|
||||
?: throw NotFoundException("공지사항이 없습니다.")
|
||||
|
||||
@Operation(summary = "공지사항 상세조회")
|
||||
@GetMapping("/{notice_id}")
|
||||
fun getNotice(@PathVariable("notice_id") noticeId: Long,
|
||||
) = getNoticeQuery.getNoticeDetail(noticeId)
|
||||
?.let { DataResponse(data = it) }
|
||||
?: throw NotFoundException("조회되는 공지사항이 없습니다.")
|
||||
|
||||
@Operation(summary = "test")
|
||||
@GetMapping("/test")
|
||||
fun test() = DataResponse(data = "test")
|
||||
fun test() = "test"
|
||||
|
||||
}
|
||||
@@ -12,4 +12,7 @@ dependencies {
|
||||
implementation(project(":util:common-util"))
|
||||
implementation(project(":core:demo-reactive-core"))
|
||||
implementation(project(":infrastructure:datastore-mongodb-reactive"))
|
||||
|
||||
// implementation(project(":infrastructure:datastore-mariadb-reactive"))
|
||||
// implementation("io.netty:netty-resolver-dns-native-macos:4.1.86.Final:osx-aarch_64")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package me.jiniworld.demohx.config.exception
|
||||
|
||||
data class ErrorResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
package me.jiniworld.demohx.config.exception
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.converter.HttpMessageConversionException
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException
|
||||
import javax.validation.ValidationException
|
||||
|
||||
@RestControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@ExceptionHandler(ServerException::class)
|
||||
fun handleServerException(ex: ServerException) : ResponseEntity<ErrorResponse> {
|
||||
logger.error { ex.message }
|
||||
|
||||
return ResponseEntity.status(ex.code).body(ErrorResponse(code = ex.code, message = ex.message))
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleException(ex: Exception): ResponseEntity<ErrorResponse> {
|
||||
logger.error { ex.message }
|
||||
|
||||
var message: String?
|
||||
var code: Int
|
||||
|
||||
when(ex) {
|
||||
is MethodArgumentTypeMismatchException -> {
|
||||
message = String.format("지원하지 않는 %s 입니다. 입력 가능한 값: %s",
|
||||
ex.name, ex.requiredType?.enumConstants?.joinToString { it.toString() })
|
||||
code = 400
|
||||
}
|
||||
is ValidationException, is HttpMessageConversionException, is IllegalArgumentException, is ArrayIndexOutOfBoundsException -> {
|
||||
message = ex.message
|
||||
code = 400
|
||||
}
|
||||
else -> {
|
||||
message = "Internal Server Error";
|
||||
code = 500
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.status(code).body(ErrorResponse(code = code, message = message ?: ""))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package me.jiniworld.demohx.config.exception
|
||||
|
||||
sealed class ServerException(
|
||||
val code: Int,
|
||||
override val message: String,
|
||||
) : RuntimeException(message)
|
||||
|
||||
data class NotFoundException(
|
||||
override val message: String,
|
||||
) : ServerException(404, message)
|
||||
|
||||
data class UnauthorizedException(
|
||||
override val message: String = "인증 정보가 잘못되었습니다",
|
||||
) : ServerException(401, message)
|
||||
@@ -0,0 +1,27 @@
|
||||
package me.jiniworld.demohx.web.item
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import me.jiniworld.demohx.annotation.WebAdapter
|
||||
import me.jiniworld.demohx.application.item.port.input.GetItemQuery
|
||||
import me.jiniworld.demohx.application.item.port.input.GetItemsCommand
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@WebAdapter
|
||||
@Tag(name = "item", description = "상품")
|
||||
@RestController
|
||||
@RequestMapping("/v1/items")
|
||||
internal class GetItemController(
|
||||
private val getItemQuery: GetItemQuery,
|
||||
) {
|
||||
@Operation(summary = "상품 목록")
|
||||
@GetMapping("")
|
||||
fun getItems(
|
||||
@RequestParam(value = "page", required = false, defaultValue = "0") page: Int,
|
||||
@RequestParam(value = "size", required = false, defaultValue = "10") size: Int,
|
||||
) = getItemQuery.getItems(GetItemsCommand(page = page, size = size))
|
||||
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import me.jiniworld.demohx.annotation.WebAdapter
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticeQuery
|
||||
import me.jiniworld.demohx.application.notice.port.input.GetNoticesCommand
|
||||
import me.jiniworld.demohx.model.DataResponse
|
||||
import me.jiniworld.demohx.model.NotFoundException
|
||||
import org.springframework.validation.annotation.Validated
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@Validated
|
||||
@WebAdapter
|
||||
@Tag(name = "setting-system", description = "설정-시스템(공지사항, FAQ, 이용약관, 메타정보 등)")
|
||||
@RestController
|
||||
@@ -19,18 +20,15 @@ internal class GetNoticeController(
|
||||
|
||||
@Operation(summary = "공지사항 목록")
|
||||
@GetMapping("")
|
||||
suspend fun getNotices(
|
||||
fun getNotices(
|
||||
@RequestParam(value = "page", required = false, defaultValue = "0") page: Int,
|
||||
@RequestParam(value = "size", required = false, defaultValue = "10") size: Int,
|
||||
) = getNoticeQuery.getNoticeSimples(GetNoticesCommand(page = page, size = size))
|
||||
?.let { DataResponse(data = it) }
|
||||
?: throw NotFoundException("공지사항이 없습니다.")
|
||||
) = getNoticeQuery.getNoticeSummaries(GetNoticesCommand(page = page, size = size))
|
||||
|
||||
@Operation(summary = "공지사항 상세조회")
|
||||
@GetMapping("/{notice_id}")
|
||||
suspend fun getNotice(@PathVariable("notice_id") noticeId: String,
|
||||
) = getNoticeQuery.getNoticeDetail(noticeId)
|
||||
?.let { DataResponse(data = it) }
|
||||
?: throw NotFoundException("조회되는 공지사항이 없습니다.")
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package me.jiniworld.demohx.web.notice
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import me.jiniworld.demohx.annotation.WebAdapter
|
||||
import me.jiniworld.demohx.application.notice.port.input.RegisterNoticeCommand
|
||||
import me.jiniworld.demohx.application.notice.port.input.RegisterNoticeUseCase
|
||||
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 javax.validation.Valid
|
||||
|
||||
@WebAdapter
|
||||
@Tag(name = "setting-system", description = "설정-시스템(공지사항, FAQ, 이용약관, 메타정보 등)")
|
||||
@RestController
|
||||
@RequestMapping("/v1/notices")
|
||||
internal class RegisterNoticeController(
|
||||
private val registerNoticeUseCase: RegisterNoticeUseCase,
|
||||
) {
|
||||
|
||||
@Operation(summary = "공지사항 등록")
|
||||
@PostMapping("")
|
||||
suspend fun getNotices(
|
||||
@Valid @RequestBody command: RegisterNoticeCommand,
|
||||
) = registerNoticeUseCase.registerNotice(command)
|
||||
|
||||
}
|
||||
@@ -7,12 +7,27 @@ spring:
|
||||
import:
|
||||
- demo-reactive-app.yml
|
||||
|
||||
# r2dbc:
|
||||
# url: r2dbc:mariadb://localhost:3306/demofx
|
||||
# username: test
|
||||
# password: test
|
||||
# pool:
|
||||
# max-size: 50
|
||||
# validation-query: SELECT 1
|
||||
# initial-size: 10
|
||||
# enabled: true
|
||||
|
||||
# h2:
|
||||
# console:
|
||||
# enabled: true
|
||||
# path: /h2-console
|
||||
|
||||
data:
|
||||
mongodb:
|
||||
uri: mongodb://localhost:27017/book
|
||||
uri: mongodb://localhost:27017/demofx
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
port: 8081
|
||||
tomcat:
|
||||
basedir: .
|
||||
# accesslog:
|
||||
@@ -48,6 +63,7 @@ logging:
|
||||
pattern:
|
||||
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(${LOG_LEVEL_PATTERN:%-5p}){green} %clr([%22thread]){magenta} %clr(%-40.40logger{39}){cyan} %clr(: %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}){faint}'
|
||||
level:
|
||||
web: DEBUG
|
||||
me.jiniworld.demohx: DEBUG
|
||||
org:
|
||||
springframework:
|
||||
|
||||
@@ -3,13 +3,12 @@ rootProject.name = "demo-hexagonal"
|
||||
|
||||
include(
|
||||
"core:demo-core",
|
||||
"core:demo-reactive-core",
|
||||
"infrastructure:datastore-mariadb",
|
||||
"infrastructure:datastore-mongodb-reactive",
|
||||
"infrastructure:datastore-mariadb-reactive",
|
||||
"server:demo-app",
|
||||
"server:demo-reactive-app",
|
||||
"util:common-util",
|
||||
"server:demo-all-in-one-app"
|
||||
)
|
||||
include("server:demo-reactive-app")
|
||||
findProject(":server:demo-reactive-app")?.name = "demo-reactive-app"
|
||||
include("core:demo-reactive-core")
|
||||
findProject(":core:demo-reactive-core")?.name = "demo-reactive-core"
|
||||
)
|
||||
Reference in New Issue
Block a user