Skip to content

Commit

Permalink
feat: 뉴스 컨텐츠 수정 기능 추가 (#64) (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
seokjin8678 authored Jan 2, 2024
1 parent 3368185 commit aa33005
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 19 deletions.
9 changes: 9 additions & 0 deletions src/docs/asciidoc/news-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ include::{snippets}/news/find-detail/query-parameters.adoc[]
*응답*
include::{snippets}/news/find-detail/http-response.adoc[]
include::{snippets}/news/find-detail/response-fields.adoc[]

[[news-content-update]]
=== 뉴스 컨텐츠 수정
*요청*
include::{snippets}/news/update-content/http-request.adoc[]
include::{snippets}/news/update-content/path-parameters.adoc[]
include::{snippets}/news/update-content/request-fields.adoc[]
*응답*
include::{snippets}/news/update-content/http-response.adoc[]
23 changes: 17 additions & 6 deletions src/main/kotlin/kr/galaxyhub/sc/api/v1/news/NewsControllerV1.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kr.galaxyhub.sc.api.v1.news
import java.util.UUID
import kr.galaxyhub.sc.api.common.ApiResponse
import kr.galaxyhub.sc.api.v1.news.dto.NewsCreateRequest
import kr.galaxyhub.sc.api.v1.news.dto.NewsUpdateRequest
import kr.galaxyhub.sc.common.support.toUri
import kr.galaxyhub.sc.news.application.NewsCommandService
import kr.galaxyhub.sc.news.application.NewsQueryService
Expand Down Expand Up @@ -32,12 +33,12 @@ class NewsControllerV1(
.body(ApiResponse.success(response))
}

@GetMapping("/{id}")
@GetMapping("/{newsId}")
fun findDetailById(
@PathVariable id: UUID,
@PathVariable newsId: UUID,
@RequestParam language: Language,
): ResponseEntity<ApiResponse<NewsDetailResponse>> {
val response = newsQueryService.getDetailByIdAndLanguage(id, language)
val response = newsQueryService.getDetailByIdAndLanguage(newsId, language)
return ResponseEntity.ok()
.body(ApiResponse.success(response))
}
Expand All @@ -46,8 +47,18 @@ class NewsControllerV1(
fun create(
@RequestBody request: NewsCreateRequest,
): ResponseEntity<ApiResponse<UUID>> {
val id = newsCommandService.create(request.toCommand())
return ResponseEntity.created("/api/v1/news/${id}".toUri())
.body(ApiResponse.success(id))
val newsId = newsCommandService.create(request.toCommand())
return ResponseEntity.created("/api/v1/news/${newsId}".toUri())
.body(ApiResponse.success(newsId))
}

@PostMapping("/{newsId}/content")
fun updateContent(
@PathVariable newsId: UUID,
@RequestBody request: NewsUpdateRequest,
): ResponseEntity<ApiResponse<Unit>> {
newsCommandService.updateContent(newsId, request.toCommand())
return ResponseEntity.ok()
.body(ApiResponse.success(Unit))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.galaxyhub.sc.api.v1.news.dto

import kr.galaxyhub.sc.news.application.NewsUpdateCommand
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.news.domain.NewsInformation

data class NewsUpdateRequest(
val language: Language,
val title: String?,
val excerpt: String?,
val content: String?,
) {

fun toCommand(): NewsUpdateCommand {
return NewsUpdateCommand(
language = language,
newsInformation = title?.let {
NewsInformation(title, excerpt)
},
content = content
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kr.galaxyhub.sc.news.domain.News
import kr.galaxyhub.sc.news.domain.NewsInformation
import kr.galaxyhub.sc.news.domain.NewsRepository
import kr.galaxyhub.sc.news.domain.NewsType
import kr.galaxyhub.sc.news.domain.getFetchByIdAndLanguage
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

Expand All @@ -33,6 +34,18 @@ class NewsCommandService(
excerpt = command.excerpt
),
)

fun updateContent(newsId: UUID, command: NewsUpdateCommand) {
val (language, newsInformation, content) = command
val news = newsRepository.getFetchByIdAndLanguage(newsId, language)
val contentByLanguage = news.getContentByLanguage(language)
newsInformation?.also {
contentByLanguage.updateNewsInformation(it)
}
content?.also {
contentByLanguage.updateContent(it)
}
}
}

data class NewsCreateCommand(
Expand All @@ -53,3 +66,9 @@ data class NewsCreateCommand(
originUrl = originUrl
)
}

data class NewsUpdateCommand(
val language: Language,
val newsInformation: NewsInformation?,
val content: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kr.galaxyhub.sc.news.application.dto.NewsDetailResponse
import kr.galaxyhub.sc.news.application.dto.NewsResponse
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.news.domain.NewsRepository
import kr.galaxyhub.sc.news.domain.getByDetailByIdAndLanguage
import kr.galaxyhub.sc.news.domain.getFetchByIdAndLanguage
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

Expand All @@ -21,7 +21,7 @@ class NewsQueryService(
}

fun getDetailByIdAndLanguage(id: UUID, language: Language): NewsDetailResponse {
return newsRepository.getByDetailByIdAndLanguage(id, language)
return newsRepository.getFetchByIdAndLanguage(id, language)
.let {
val content = it.getContentByLanguage(language)
NewsDetailResponse.of(it, content)
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/news/domain/Content.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ class Content(
@Column(name = "content", nullable = false)
var content: String = content
protected set

fun updateNewsInformation(newsInformation: NewsInformation) {
this.newsInformation = newsInformation
}

fun updateContent(content: String) {
this.content = content
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.Repository
import org.springframework.data.repository.query.Param

fun NewsRepository.getByDetailByIdAndLanguage(id: UUID, language: Language) = findFetchByIdAndLanguage(id, language)
fun NewsRepository.getFetchByIdAndLanguage(id: UUID, language: Language) = findFetchByIdAndLanguage(id, language)
?: throw NotFoundException("식별자와 언어에 대한 뉴스를 찾을 수 없습니다. id=${id}, language=${language}")

fun NewsRepository.getOrThrow(id: UUID) = findById(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.util.UUID
import kr.galaxyhub.sc.common.support.validate
import kr.galaxyhub.sc.news.application.dto.NewsAppendContentEvent
import kr.galaxyhub.sc.news.domain.NewsRepository
import kr.galaxyhub.sc.news.domain.getByDetailByIdAndLanguage
import kr.galaxyhub.sc.news.domain.getFetchByIdAndLanguage
import kr.galaxyhub.sc.translation.application.dto.TranslationCommand
import kr.galaxyhub.sc.translation.application.dto.TranslatorFailureEvent
import kr.galaxyhub.sc.translation.domain.TranslateProgression
Expand All @@ -28,7 +28,7 @@ class TranslationCommandService(

fun translate(command: TranslationCommand): UUID {
val (newsId, sourceLanguage, targetLanguage, translatorProvider) = command
val news = newsRepository.getByDetailByIdAndLanguage(newsId, sourceLanguage)
val news = newsRepository.getFetchByIdAndLanguage(newsId, sourceLanguage)
validate(news.isSupportLanguage(targetLanguage)) { "이미 뉴스에 번역된 컨텐츠가 존재합니다. targetLanguage=$targetLanguage" }
val content = news.getContentByLanguage(sourceLanguage)
val translateProgression = TranslateProgression(newsId, sourceLanguage, targetLanguage, translatorProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockkBean
import io.kotest.core.spec.style.DescribeSpec
import io.mockk.every
import io.mockk.justRun
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import kr.galaxyhub.sc.api.v1.news.dto.NewsCreateRequest
import kr.galaxyhub.sc.api.support.ARRAY
import kr.galaxyhub.sc.api.support.ENUM
import kr.galaxyhub.sc.api.support.NUMBER
Expand All @@ -21,6 +21,8 @@ import kr.galaxyhub.sc.api.support.docPost
import kr.galaxyhub.sc.api.support.param
import kr.galaxyhub.sc.api.support.pathMeans
import kr.galaxyhub.sc.api.support.type
import kr.galaxyhub.sc.api.v1.news.dto.NewsCreateRequest
import kr.galaxyhub.sc.api.v1.news.dto.NewsUpdateRequest
import kr.galaxyhub.sc.news.application.NewsCommandService
import kr.galaxyhub.sc.news.application.NewsQueryService
import kr.galaxyhub.sc.news.application.dto.NewsDetailResponse
Expand All @@ -43,21 +45,21 @@ class NewsControllerV1Test(
private val newsCommandService: NewsCommandService,
) : DescribeSpec({

describe("GET /api/v1/news/{id}") {
describe("GET /api/v1/news/{newsId}") {
context("유효한 요청이 전달되면") {
val id = UUID.randomUUID()
val response = newsDetailResponse(id)
every { newsQueryService.getDetailByIdAndLanguage(id, Language.ENGLISH) } returns response
val newsId = UUID.randomUUID()
val response = newsDetailResponse(newsId)
every { newsQueryService.getDetailByIdAndLanguage(newsId, Language.ENGLISH) } returns response

it("200 응답과 뉴스의 상세 정보가 조회된다.") {
mockMvc.docGet("/api/v1/news/{id}", id) {
mockMvc.docGet("/api/v1/news/{newsId}", newsId) {
contentType = MediaType.APPLICATION_JSON
param("language" to Language.ENGLISH)
}.andExpect {
status { isOk() }
}.andDocument("news/find-detail") {
pathParameters(
"id" pathMeans "뉴스 식별자" constraint "UUID"
"newsId" pathMeans "뉴스 식별자" constraint "UUID"
)
queryParameters(
"language" pathMeans "뉴스 언어" formattedAs ENUM(Language::class)
Expand Down Expand Up @@ -136,8 +138,42 @@ class NewsControllerV1Test(
}
}
}

describe("POST /api/v1/news/{newsId}/content") {
context("유효한 요청이 전달되면") {
val newsId = UUID.randomUUID()
val request = newsUpdateRequest()
justRun { newsCommandService.updateContent(any(), any()) }

it("200 응답이 반환된다.") {
mockMvc.docPost("/api/v1/news/{newsId}/content", newsId) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(request)
}.andExpect {
status { isOk() }
}.andDocument("news/update-content") {
pathParameters(
"newsId" pathMeans "뉴스 식별자" constraint "UUID"
)
requestBody(
"language" type ENUM(Language::class) means "뉴스 언어",
"title" type STRING means "뉴스 제목" isOptional true,
"excerpt" type STRING means "뉴스 발췌" isOptional true,
"content" type STRING means "뉴스 내용" isOptional true
)
}
}
}
}
})

private fun newsUpdateRequest() = NewsUpdateRequest(
language = Language.ENGLISH,
title = "Star Citizen Live",
excerpt = "You asked. We're answering! Join us today for a live Q&A show with the Vehicle Gameplay team.",
content = "blah blah"
)

private fun newsCreateRequest() = NewsCreateRequest(
newsType = NewsType.NEWS,
title = "Star Citizen Live",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package kr.galaxyhub.sc.news.application

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import java.time.LocalDateTime
import java.time.ZoneId
Expand All @@ -11,14 +13,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.news.domain.MemoryNewsRepository
import kr.galaxyhub.sc.news.domain.NewsInformation
import kr.galaxyhub.sc.news.domain.NewsType
import kr.galaxyhub.sc.news.fixture.ContentFixture
import kr.galaxyhub.sc.news.fixture.NewsFixture

class NewsCommandServiceTest(
private val newsRepository: MemoryNewsRepository = MemoryNewsRepository(),
private val newsCommandService: NewsCommandService = NewsCommandService(newsRepository),
) : DescribeSpec({

beforeContainer { newsRepository.clear() }
isolationMode = IsolationMode.InstancePerLeaf

describe("create") {
context("originId에 대해 저장된 뉴스가 없으면") {
Expand Down Expand Up @@ -53,6 +58,52 @@ class NewsCommandServiceTest(
}
}
}

describe("updateContent") {
val news = NewsFixture.create()
val content = ContentFixture.create(
newsId = news.id,
language = Language.ENGLISH,
newsInformation = NewsInformation("old", "old"),
content = "old",
)
news.addContent(content)
newsRepository.save(news)

context("Command의 newsInformation이 null이면") {
val command = NewsUpdateCommand(
language = Language.ENGLISH,
newsInformation = null,
content = "update"
)
newsCommandService.updateContent(news.id, command)

it("newsInformation은 수정되지 않는다.") {
content.newsInformation shouldBe NewsInformation("old", "old")
}

it("content는 수정된다.") {
content.content shouldBe "update"
}
}

context("Command의 content가 null이면") {
val command = NewsUpdateCommand(
language = Language.ENGLISH,
newsInformation = NewsInformation("update", "update"),
content = null
)
newsCommandService.updateContent(news.id, command)

it("content는 수정되지 않는다.") {
content.content shouldBe "old"
}

it("newsInformation은 수정된다.") {
content.newsInformation shouldBe NewsInformation("update", "update")
}
}
}
})

private fun newsCreateCommand(originId: Long) = NewsCreateCommand(
Expand Down

0 comments on commit aa33005

Please sign in to comment.