17 Commits

Author SHA1 Message Date
jini
5a4a6b3355 - 2023-03-31 03:28:19 +09:00
jini
59cf934058 🎨 GetNoticesCommand validation 설정 추가 2023-03-31 02:51:50 +09:00
jini
02553cdec7 🔧 web logging level 설정: debug 2023-03-31 02:51:01 +09:00
jini
7d3b445303 hibernate-validator 의존성 추가 2023-03-31 02:50:28 +09:00
jini
c7f6e3ca81 🎨 global exception handler 추가 2023-03-31 02:49:05 +09:00
jini
9ff1c6ecec 공지사항 등록 2023-03-31 01:36:23 +09:00
jini
022e344fea 🐛 공지사항 등록시 id가 빈문자열로 등록되는 bug fix 2023-03-31 01:36:10 +09:00
jini
a2b580e8e7 🚚 2023-03-30 19:57:52 +09:00
jini
36c34a77c4 🎨 [reactive-app] domain mapper 클래스 추가 2023-03-30 19:56:46 +09:00
jini
4e2cc2ea10 [reactive-app] 상품 조회 2023-03-28 04:14:07 +09:00
jini
122766f76e 🔥 2023-03-28 04:13:34 +09:00
jini
6ed8a0e62a 🔥 2023-03-28 03:06:45 +09:00
jini
b399db7604 🎨 2023-03-21 01:20:15 +09:00
jini
78e8f9b7ec 속도 개선을 위해 domain 수정 2023-03-07 23:20:36 +09:00
jini
c675c10417 🔧 2023-03-07 23:20:11 +09:00
jini
ec07b2c637 🔧 demo-reactive-app 의 datastore 의존성 변경: datastore-mongo-reactive -> datastore-mariadb-reactive 2023-03-07 21:18:49 +09:00
jini
d3eb581d94 datastore-mariadb-reactive 추가 2023-03-07 21:18:07 +09:00
47 changed files with 594 additions and 95 deletions

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>?

View File

@@ -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?
}

View File

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

View File

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

View File

@@ -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,
)

View File

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

View File

@@ -0,0 +1,6 @@
package me.jiniworld.demohx.application.item.port.input
data class GetItemsCommand(
val page: Int,
val size: Int,
)

View File

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

View File

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

View File

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

View File

@@ -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?
}

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -0,0 +1,5 @@
package me.jiniworld.demohx.application.notice.port.input
interface RegisterNoticeUseCase {
suspend fun registerNotice(command: RegisterNoticeCommand)
}

View File

@@ -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,
)

View File

@@ -1,7 +0,0 @@
package me.jiniworld.demohx.application.notice.port.output
data class NoticeSimple(
val id: String,
val title: String,
val createdOn: String,
)

View File

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

View File

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

View File

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

View 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"))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

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

View File

@@ -0,0 +1,6 @@
package me.jiniworld.demohx.config.exception
data class ErrorResponse(
val code: Int,
val message: String,
)

View File

@@ -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 ?: ""))
}
}

View File

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

View File

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

View File

@@ -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("조회되는 공지사항이 없습니다.")
}

View File

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

View File

@@ -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:

View File

@@ -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"
)