Skip to content

Commit

Permalink
feat: 번역 도메인 API, 엔티티 설계 (#54) (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
seokjin8678 authored Dec 23, 2023
1 parent 870603e commit 326de14
Show file tree
Hide file tree
Showing 14 changed files with 332 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ include::news-api.adoc[]
include::crawler-api.adoc[]

include::auth-api.adoc[]

include::translation-api.adoc[]
25 changes: 25 additions & 0 deletions src/docs/asciidoc/translation-api.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[[translation-api]]
== 번역

[[translation-translate]]
=== 뉴스 번역 요청

*요청*
include::{snippets}/translation/translate/http-request.adoc[]
include::{snippets}/translation/translate/path-parameters.adoc[]
include::{snippets}/translation/translate/request-fields.adoc[]

*응답*
include::{snippets}/translation/translate/http-response.adoc[]
include::{snippets}/translation/translate/response-fields.adoc[]

[[translation-find-by-id]]
=== 번역 진행 상황 단건 조회

*요청*
include::{snippets}/translation/find-by-id/http-request.adoc[]
include::{snippets}/translation/find-by-id/path-parameters.adoc[]

*응답*
include::{snippets}/translation/find-by-id/http-response.adoc[]
include::{snippets}/translation/find-by-id/response-fields.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kr.galaxyhub.sc.api.v1.translation

import java.util.UUID
import kr.galaxyhub.sc.api.common.ApiResponse
import kr.galaxyhub.sc.api.v1.translation.dto.TranslationRequest
import kr.galaxyhub.sc.common.support.toUri
import kr.galaxyhub.sc.translation.application.TranslationCommandService
import kr.galaxyhub.sc.translation.application.TranslationQueryService
import kr.galaxyhub.sc.translation.application.dto.TranslationCommand
import kr.galaxyhub.sc.translation.application.dto.TranslationResponse
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1/translation")
class TranslationControllerV1(
private val translationCommandService: TranslationCommandService,
private val translationQueryService: TranslationQueryService,
) {

@PostMapping("/{newsId}")
fun translate(
@PathVariable newsId: UUID,
@RequestBody request: TranslationRequest,
): ResponseEntity<ApiResponse<UUID>> {
val command = TranslationCommand(newsId, request.destinationLanguage)
val translateProgressionId = translationCommandService.translate(command)
return ResponseEntity.created("/api/v1/translation/${translateProgressionId}".toUri())
.body(ApiResponse.success(translateProgressionId))
}

@GetMapping("/{translateProgressionId}")
fun findById(
@PathVariable translateProgressionId: UUID,
): ResponseEntity<ApiResponse<TranslationResponse>> {
val response = translationQueryService.findById(translateProgressionId)
return ResponseEntity.ok()
.body(ApiResponse.success(response))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.galaxyhub.sc.api.v1.translation.dto

import kr.galaxyhub.sc.news.domain.Language

data class TranslationRequest(
val destinationLanguage: Language
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.galaxyhub.sc.translation.application

import java.util.UUID
import kr.galaxyhub.sc.translation.application.dto.TranslationCommand
import kr.galaxyhub.sc.translation.domain.TranslateProgression
import kr.galaxyhub.sc.translation.domain.TranslationProgressionRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class TranslationCommandService(
private val translationProgressionRepository: TranslationProgressionRepository
) {

fun translate(command: TranslationCommand): UUID {
val newsId = command.newsId
val destinationLanguage = command.destinationLanguage
val translateProgression = TranslateProgression(newsId, destinationLanguage)
translationProgressionRepository.save(translateProgression)
return translateProgression.id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package kr.galaxyhub.sc.translation.application

import java.util.UUID
import kr.galaxyhub.sc.translation.application.dto.TranslationResponse
import kr.galaxyhub.sc.translation.domain.TranslationProgressionRepository
import kr.galaxyhub.sc.translation.domain.getOrThrow
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class TranslationQueryService(
private val translationProgressionRepository: TranslationProgressionRepository,
) {

fun findById(translateProgressionId: UUID): TranslationResponse {
return translationProgressionRepository.getOrThrow(translateProgressionId)
.let { TranslationResponse.from(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.galaxyhub.sc.translation.application.dto

import java.util.UUID
import kr.galaxyhub.sc.news.domain.Language

data class TranslationCommand(
val newsId: UUID,
val destinationLanguage: Language
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.galaxyhub.sc.translation.application.dto

import java.util.UUID
import kr.galaxyhub.sc.translation.domain.TranslateProgression
import kr.galaxyhub.sc.translation.domain.TranslationStatus

data class TranslationResponse(
val translateProgressionId: UUID,
val targetNewsId: UUID,
val translationStatus: TranslationStatus,
val message: String? = null,
) {

companion object {

fun from(translationProgression: TranslateProgression): TranslationResponse {
return TranslationResponse(
translateProgressionId = translationProgression.id,
targetNewsId = translationProgression.newsId,
translationStatus = translationProgression.translationStatus,
message = translationProgression.message
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kr.galaxyhub.sc.translation.domain

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import java.util.UUID
import kr.galaxyhub.sc.common.domain.PrimaryKeyEntity
import kr.galaxyhub.sc.news.domain.Language

@Entity
@Table(
uniqueConstraints = [
UniqueConstraint(
name = "UNIQUE_NEWS_ID_AND_DESTINATION_LANGUAGE",
columnNames = [
"news_id",
"destination_language"
]
)
]
)
class TranslateProgression(
newsId: UUID,
destinationLanguage: Language,
) : PrimaryKeyEntity() {

@Column(name = "news_id", nullable = false, columnDefinition = "uuid")
val newsId: UUID = newsId

@Enumerated(EnumType.STRING)
@Column(name = "destination_language", nullable = false, columnDefinition = "varchar")
val destinationLanguage: Language = destinationLanguage

@Enumerated(EnumType.STRING)
@Column(name = "translation_status", nullable = false, columnDefinition = "varchar")
var translationStatus: TranslationStatus = TranslationStatus.PROGRESS
protected set

@Column(name = "message")
var message: String? = null
protected set
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.galaxyhub.sc.translation.domain

import java.util.UUID
import kr.galaxyhub.sc.common.exception.NotFoundException
import org.springframework.data.repository.Repository

fun TranslationProgressionRepository.getOrThrow(translateProgressionId: UUID) = findById(translateProgressionId)
?: throw NotFoundException("식별자에 대한 번역 진행 상황을 찾을 수 없습니다. id=$translateProgressionId")

interface TranslationProgressionRepository : Repository<TranslateProgression, UUID> {

fun save(translateProgression: TranslateProgression): TranslateProgression

fun findById(translateProgressionId: UUID): TranslateProgression?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.galaxyhub.sc.translation.domain

enum class TranslationStatus {
PROGRESS,
COMPLETE,
ERROR,
}
12 changes: 12 additions & 0 deletions src/main/resources/db/migration/V3__add_translate_progression.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE translate_progression
(
id BINARY(36) NOT NULL,
news_id BINARY(36) NOT NULL,
destination_language VARCHAR(255) NOT NULL,
translation_status VARCHAR(255) NOT NULL,
message VARCHAR(255) NULL,
CONSTRAINT pk_translate_progression PRIMARY KEY (id)
);

ALTER TABLE translate_progression
ADD CONSTRAINT UNIQUE_NEWS_ID_AND_DESTINATION_LANGUAGE UNIQUE (news_id, destination_language);
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package kr.galaxyhub.sc.api.v1.translation

import com.fasterxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockkBean
import io.kotest.core.spec.style.DescribeSpec
import io.mockk.every
import java.util.UUID
import kr.galaxyhub.sc.api.support.ENUM
import kr.galaxyhub.sc.api.support.STRING
import kr.galaxyhub.sc.api.support.andDocument
import kr.galaxyhub.sc.api.support.docGet
import kr.galaxyhub.sc.api.support.docPost
import kr.galaxyhub.sc.api.support.pathMeans
import kr.galaxyhub.sc.api.support.type
import kr.galaxyhub.sc.api.v1.translation.dto.TranslationRequest
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.translation.application.TranslationCommandService
import kr.galaxyhub.sc.translation.application.TranslationQueryService
import kr.galaxyhub.sc.translation.application.dto.TranslationResponse
import kr.galaxyhub.sc.translation.domain.TranslationStatus
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc

@WebMvcTest(TranslationControllerV1::class)
@AutoConfigureRestDocs
class TranslationControllerV1Test(
private val mockMvc: MockMvc,
private val objectMapper: ObjectMapper,
@MockkBean
private val translationCommandService: TranslationCommandService,
@MockkBean
private val translationQueryService: TranslationQueryService,
) : DescribeSpec({

describe("POST /api/v1/translation/{newsId}") {
context("유효한 요청이 전달되면") {
val request = TranslationRequest(Language.KOREAN)
val newsId = UUID.randomUUID()
every { translationCommandService.translate(any()) } returns UUID.randomUUID()

it("201 응답과 번역 진행 상황의 식별자가 반환된다.") {
mockMvc.docPost("/api/v1/translation/{newsId}", newsId) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(request)
}.andExpect {
status { isCreated() }
}.andDocument("translation/translate") {
pathParameters(
"newsId" pathMeans "번역할 뉴스의 식별자"
)
requestBody(
"destinationLanguage" type ENUM(Language::class) means "번역 도착 언어"
)
responseBody(
"data" type STRING means "번역 진행 상황의 식별자"
)
}
}
}
}

describe("GET /api/v1/translation/{translateProgressionId}") {
context("유효한 요청이 전달되면") {
val translateProgressionId = UUID.randomUUID()
val response = translationResponse(translateProgressionId)
every { translationQueryService.findById(any()) } returns response

it("200 응답과 번역 진행 상황의 정보가 조회된다.") {
mockMvc.docGet("/api/v1/translation/{translateProgressionId}", translateProgressionId) {
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status { isOk() }
}.andDocument("translation/find-by-id") {
pathParameters(
"translateProgressionId" pathMeans "번역 진행 상황의 식별자"
)
responseBody(
"data.translateProgressionId" type STRING means "번역 진행 상황의 식별자",
"data.targetNewsId" type STRING means "번역할 뉴스의 식별자",
"data.translationStatus" type ENUM(TranslationStatus::class) means "번역 상태",
"data.message" type STRING means "번역 진행 상황의 추가적 메시지" isOptional true
)
}
}
}
}
})

private fun translationResponse(translateProgressionId: UUID) = TranslationResponse(
translateProgressionId = translateProgressionId,
targetNewsId = UUID.randomUUID(),
translationStatus = TranslationStatus.PROGRESS,
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

==== Response Fields
|===
|필드명|타입|설명|null여부
|필드명|타입|설명|null여부|형식

{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}true{{/optional}}{{^optional}}{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{#format}}{{format}}{{/format}}{{^format}}{{/format}}{{/tableCellContent}}
{{/fields}}
|===

0 comments on commit 326de14

Please sign in to comment.