diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/exception/LetterException.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/exception/LetterException.kt index 4d76a74..85353b2 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/exception/LetterException.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/exception/LetterException.kt @@ -11,34 +11,34 @@ sealed class LetterException( class SendLetterNotFoundException( message: String = "존재하지 않는 편지입니다.", ) : LetterException( - errorCode = 1, - message = message, - httpStatus = 404, - ) + errorCode = 1, + message = message, + httpStatus = 404, + ) class InvalidLetterAccessException( message: String = "편지에 대한 접근 권한이 없습니다.", ) : LetterException( - errorCode = 2, - message = message, - httpStatus = 403, - ) + errorCode = 2, + message = message, + httpStatus = 403, + ) class ReceiveLetterNotFoundException( message: String = "존재하지 않는 편지입니다.", ) : LetterException( - errorCode = 3, - message = message, - httpStatus = 404, - ) + errorCode = 3, + message = message, + httpStatus = 404, + ) class DraftLetterNotFoundException( message: String = "존재하지 않는 임시 편지입니다.", ) : LetterException( - errorCode = 4, - message = message, - httpStatus = 404, - ) + errorCode = 4, + message = message, + httpStatus = 404, + ) companion object { const val CODE_PREFIX = "LETTER" diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/UpdateDraftLetterUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/UpdateDraftLetterUsecase.kt index 813c77e..4af9cd5 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/UpdateDraftLetterUsecase.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/UpdateDraftLetterUsecase.kt @@ -1,13 +1,25 @@ package com.asap.application.letter.port.`in` interface UpdateDraftLetterUsecase { - fun command(command: Command) - - data class Command( - val draftId: String, - val userId: String, - val content: String, - val receiverName: String, - val images: List, - ) + fun command(command: Command.Send) + + fun command(command: Command.Physical) + + sealed class Command { + data class Send( + val draftId: String, + val userId: String, + val content: String, + val images: List, + val receiverName: String, + ) : Command() + + data class Physical( + val draftId: String, + val userId: String, + val content: String, + val images: List, + val senderName: String, + ) : Command() + } } diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/port/out/ReceiveDraftLetterManagementPort.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/port/out/ReceiveDraftLetterManagementPort.kt index 2a916a4..b169562 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/port/out/ReceiveDraftLetterManagementPort.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/port/out/ReceiveDraftLetterManagementPort.kt @@ -1,7 +1,10 @@ package com.asap.application.letter.port.out +import com.asap.domain.common.DomainId import com.asap.domain.letter.entity.ReceiveDraftLetter interface ReceiveDraftLetterManagementPort { fun save(receiveDraftLetter: ReceiveDraftLetter): ReceiveDraftLetter + + fun getDraftLetterNotNull(draftId: DomainId, ownerId: DomainId): ReceiveDraftLetter } \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/service/DraftLetterCommandService.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/service/DraftLetterCommandService.kt index e5b68ba..6409350 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/service/DraftLetterCommandService.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/service/DraftLetterCommandService.kt @@ -31,7 +31,7 @@ class DraftLetterCommandService( return GenerateDraftKeyUsecase.Response(receiveDraftLetter.id.value) } - override fun command(command: UpdateDraftLetterUsecase.Command) { + override fun command(command: UpdateDraftLetterUsecase.Command.Send) { val draftLetter = draftLetterManagementPort.getDraftLetterNotNull( draftId = DomainId(command.draftId), @@ -63,4 +63,19 @@ class DraftLetterCommandService( draftLetterManagementPort.remove(it) } } + + override fun command(command: UpdateDraftLetterUsecase.Command.Physical) { + val receiveDraftLetter = + receiveDraftLetterManagementPort.getDraftLetterNotNull( + draftId = DomainId(command.draftId), + ownerId = DomainId(command.userId), + ) + + receiveDraftLetter.update( + content = command.content, + senderName = command.senderName, + images = command.images, + ) + receiveDraftLetterManagementPort.save(receiveDraftLetter) + } } diff --git a/Application-Module/src/test/kotlin/com/asap/application/letter/service/DraftLetterCommandServiceTest.kt b/Application-Module/src/test/kotlin/com/asap/application/letter/service/DraftLetterCommandServiceTest.kt index 7efdd57..5c37ff8 100644 --- a/Application-Module/src/test/kotlin/com/asap/application/letter/service/DraftLetterCommandServiceTest.kt +++ b/Application-Module/src/test/kotlin/com/asap/application/letter/service/DraftLetterCommandServiceTest.kt @@ -1,5 +1,6 @@ package com.asap.application.letter.service +import com.asap.application.letter.exception.LetterException import com.asap.application.letter.port.`in`.GenerateDraftKeyUsecase import com.asap.application.letter.port.`in`.RemoveDraftLetterUsecase import com.asap.application.letter.port.`in`.UpdateDraftLetterUsecase @@ -8,6 +9,7 @@ import com.asap.application.letter.port.out.ReceiveDraftLetterManagementPort import com.asap.domain.common.DomainId import com.asap.domain.letter.entity.DraftLetter import com.asap.domain.letter.entity.ReceiveDraftLetter +import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.mockk.every @@ -16,6 +18,7 @@ import io.mockk.verify class DraftLetterCommandServiceTest : BehaviorSpec({ + isolationMode = IsolationMode.InstancePerLeaf val mockGenerateDraftKeyUsecase = mockk(relaxed = true) val mockReceiveDraftLetterManagementPort = mockk(relaxed = true) @@ -36,7 +39,7 @@ class DraftLetterCommandServiceTest : given("임시 저장 편지를 수정할 때") { val command = - UpdateDraftLetterUsecase.Command( + UpdateDraftLetterUsecase.Command.Send( draftId = "draftId", userId = "userId", content = "content", @@ -86,4 +89,50 @@ class DraftLetterCommandServiceTest : } } } + + given("받은 임시 저장편지를 수저할 때"){ + + `when`("사용자 아이디와 임시 저장 편지 아이디, 내용, 발신자 이름, 이미지를 입력하면"){ + val command = UpdateDraftLetterUsecase.Command.Physical( + draftId = "draftId", + userId = "userId", + content = "content", + images = listOf("image1", "image2"), + senderName = "senderName", + ) + val receiveDraftLetter = ReceiveDraftLetter.default(DomainId(command.userId)) + every { + mockReceiveDraftLetterManagementPort.getDraftLetterNotNull( + draftId = receiveDraftLetter.id, + ownerId = receiveDraftLetter.ownerId, + ) + } returns receiveDraftLetter + draftLetterCommandService.command(command) + then("받은 임시 저장편지를 수정한다"){ + verify { mockReceiveDraftLetterManagementPort.save(any()) } + } + } + + `when`("임시 저장 편지가 없으면"){ + + val command = UpdateDraftLetterUsecase.Command.Physical( + draftId = "draftId", + userId = "userId", + content = "content", + images = listOf("image1", "image2"), + senderName = "senderName", + ) + every { + mockReceiveDraftLetterManagementPort.getDraftLetterNotNull( + draftId = DomainId(command.draftId), + ownerId = DomainId(command.userId), + ) + } throws LetterException.DraftLetterNotFoundException() + then("임시 저장 편지가 수정되지 않는다"){ + verify(exactly = 0) { + mockReceiveDraftLetterManagementPort.save(any()) + } + } + } + } }) diff --git a/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterApplicationConfig.kt b/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterApplicationConfig.kt index 2b2fbc7..473170a 100644 --- a/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterApplicationConfig.kt +++ b/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterApplicationConfig.kt @@ -2,6 +2,7 @@ package com.asap.application.letter import com.asap.application.letter.port.out.DraftLetterManagementPort import com.asap.application.letter.port.out.IndependentLetterManagementPort +import com.asap.application.letter.port.out.ReceiveDraftLetterManagementPort import com.asap.application.letter.port.out.SendLetterManagementPort import com.asap.application.letter.port.out.SpaceLetterManagementPort import org.springframework.boot.test.context.TestConfiguration @@ -13,6 +14,7 @@ class LetterApplicationConfig( private val independentLetterManagementPort: IndependentLetterManagementPort, private val spaceLetterManagementPort: SpaceLetterManagementPort, private val draftLetterManagementPort: DraftLetterManagementPort, + private val receiveDraftLetterManagementPort: ReceiveDraftLetterManagementPort, ) { @Bean fun letterMockManager(): LetterMockManager = @@ -21,5 +23,6 @@ class LetterApplicationConfig( independentLetterManagementPort, spaceLetterManagementPort, draftLetterManagementPort, + receiveDraftLetterManagementPort ) } diff --git a/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterMockManager.kt b/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterMockManager.kt index efa696a..514881c 100644 --- a/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterMockManager.kt +++ b/Application-Module/src/testFixtures/kotlin/com/asap/application/letter/LetterMockManager.kt @@ -1,14 +1,8 @@ package com.asap.application.letter -import com.asap.application.letter.port.out.DraftLetterManagementPort -import com.asap.application.letter.port.out.IndependentLetterManagementPort -import com.asap.application.letter.port.out.SendLetterManagementPort -import com.asap.application.letter.port.out.SpaceLetterManagementPort +import com.asap.application.letter.port.out.* import com.asap.domain.common.DomainId -import com.asap.domain.letter.entity.DraftLetter -import com.asap.domain.letter.entity.IndependentLetter -import com.asap.domain.letter.entity.SendLetter -import com.asap.domain.letter.entity.SpaceLetter +import com.asap.domain.letter.entity.* import com.asap.domain.letter.service.LetterCodeGenerator import com.asap.domain.letter.vo.LetterContent import com.asap.domain.letter.vo.ReceiverInfo @@ -21,6 +15,7 @@ class LetterMockManager( private val independentLetterManagementPort: IndependentLetterManagementPort, private val spaceLetterManagementPort: SpaceLetterManagementPort, private val draftLetterManagementPort: DraftLetterManagementPort, + private val receiveDraftLetterManagementPort: ReceiveDraftLetterManagementPort, ) { private val letterCodeGenerator = LetterCodeGenerator() @@ -158,4 +153,10 @@ class LetterMockManager( draftLetterManagementPort.save(draftLetter) return draftLetter.id.value } + + fun generateMockReceiveDraftLetter(userId: String): String { + val receiveDraftLetter = ReceiveDraftLetter.default(DomainId(userId)) + receiveDraftLetterManagementPort.save(receiveDraftLetter) + return receiveDraftLetter.id.value + } } diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/DraftLetterApi.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/DraftLetterApi.kt index 65bc122..2610da5 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/DraftLetterApi.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/DraftLetterApi.kt @@ -64,6 +64,19 @@ interface DraftLetterApi { @RequestBody request: UpdateDraftLetterRequest, ) + @Operation(summary = "실물 편지 임시 저장하기") + @PostMapping("/physical/{draftId}") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "임시 저장 성공"), + ], + ) + fun updatePhysicalDraft( + @PathVariable draftId: String, + @AccessUser userId: String, + @RequestBody request: UpdatePhysicalDraftLetterRequest, + ) + @Operation(summary = "임시 저장 목록 조회") @GetMapping() @ApiResponses( diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/DraftLetterController.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/DraftLetterController.kt index a726e02..048385b 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/DraftLetterController.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/DraftLetterController.kt @@ -31,7 +31,7 @@ class DraftLetterController( request: UpdateDraftLetterRequest, ) { updateDraftLetterUsecase.command( - UpdateDraftLetterUsecase.Command( + UpdateDraftLetterUsecase.Command.Send( draftId = draftId, userId = userId, content = request.content, @@ -41,6 +41,22 @@ class DraftLetterController( ) } + override fun updatePhysicalDraft( + draftId: String, + userId: String, + request: UpdatePhysicalDraftLetterRequest + ) { + updateDraftLetterUsecase.command( + UpdateDraftLetterUsecase.Command.Physical( + draftId = draftId, + userId = userId, + content = request.content, + images = request.images, + senderName = request.senderName, + ), + ) + } + override fun getAllDrafts(userId: String): GetAllDraftLetterResponse = getDraftLetterUsecase .getAll(GetDraftLetterUsecase.Query.All(userId)) diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/UpdatePhysicalDraftLetterRequest.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/UpdatePhysicalDraftLetterRequest.kt new file mode 100644 index 0000000..1a705f0 --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/UpdatePhysicalDraftLetterRequest.kt @@ -0,0 +1,8 @@ +package com.asap.bootstrap.web.letter.dto + +data class UpdatePhysicalDraftLetterRequest( + val senderName: String, + val content: String, + val images: List +) { +} \ No newline at end of file diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/DraftLetterControllerTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/DraftLetterControllerTest.kt index 2dbb8e4..ad0f3bb 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/DraftLetterControllerTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/DraftLetterControllerTest.kt @@ -4,6 +4,7 @@ import com.asap.application.letter.port.`in`.GenerateDraftKeyUsecase import com.asap.application.letter.port.`in`.GetDraftLetterUsecase import com.asap.bootstrap.acceptance.letter.LetterAcceptanceSupporter import com.asap.bootstrap.web.letter.dto.UpdateDraftLetterRequest +import com.asap.bootstrap.web.letter.dto.UpdatePhysicalDraftLetterRequest import org.junit.jupiter.api.Test import org.mockito.BDDMockito import org.springframework.http.MediaType @@ -204,4 +205,29 @@ class DraftLetterControllerTest : LetterAcceptanceSupporter() { jsonPath("$.draftId") { isString() } } } + + @Test + fun `update physical draft`() { + // given + val userId = userMockManager.settingUser() + val accessToken = jwtMockManager.generateAccessToken(userId) + val request = + UpdatePhysicalDraftLetterRequest( + content = "content", + senderName = "senderName", + images = listOf("image"), + ) + // when + val response = + mockMvc.post("/api/v1/letters/drafts/physical/draftId") { + contentType = MediaType.APPLICATION_JSON + header("Authorization", "Bearer $accessToken") + content = objectMapper.writeValueAsString(request) + } + + // then + response.andExpect { + status { isOk() } + } + } } diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/DraftLetterApiIntegrationTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/DraftLetterApiIntegrationTest.kt index 0c91d47..a004097 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/DraftLetterApiIntegrationTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/DraftLetterApiIntegrationTest.kt @@ -1,8 +1,14 @@ package com.asap.bootstrap.integration.letter import com.asap.application.letter.LetterMockManager +import com.asap.application.letter.port.out.ReceiveDraftLetterManagementPort import com.asap.bootstrap.IntegrationSupporter import com.asap.bootstrap.web.letter.dto.UpdateDraftLetterRequest +import com.asap.bootstrap.web.letter.dto.UpdatePhysicalDraftLetterRequest +import com.asap.domain.common.DomainId +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType @@ -10,7 +16,9 @@ import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.post -class DraftLetterApiIntegrationTest : IntegrationSupporter() { +class DraftLetterApiIntegrationTest( + private val receiveDraftLetterManagementPort: ReceiveDraftLetterManagementPort +) : IntegrationSupporter() { @Autowired private lateinit var letterMockManager: LetterMockManager @@ -160,4 +168,68 @@ class DraftLetterApiIntegrationTest : IntegrationSupporter() { jsonPath("$.draftId") { isString() } } } + + @Nested + @DisplayName("update physical draft") + inner class UpdatePhysicalDraft { + @Test + fun `update physical draft`() { + // given + val userId = userMockManager.settingUser() + val accessToken = jwtMockManager.generateAccessToken(userId) + val draftId = letterMockManager.generateMockReceiveDraftLetter(userId) + val request = + UpdatePhysicalDraftLetterRequest( + content = "content", + images = listOf("image"), + senderName = "senderName", + ) + // when + val response = + mockMvc.post("/api/v1/letters/drafts/physical/$draftId") { + contentType = MediaType.APPLICATION_JSON + header("Authorization", "Bearer $accessToken") + content = objectMapper.writeValueAsString(request) + } + + // then + response.andExpect { + status { isOk() } + } + receiveDraftLetterManagementPort.getDraftLetterNotNull( + draftId = DomainId(draftId), + ownerId = DomainId(userId) + ).apply { + content shouldBe "content" + senderName shouldBe "senderName" + images shouldBe listOf("image") + } + } + + @Test + fun `throw exception when draft not found`() { + // given + val userId = userMockManager.settingUser() + val accessToken = jwtMockManager.generateAccessToken(userId) + val draftId = "notFound" + val request = + UpdatePhysicalDraftLetterRequest( + content = "content", + images = listOf("image"), + senderName = "senderName", + ) + // when + val response = + mockMvc.post("/api/v1/letters/drafts/physical/$draftId") { + contentType = MediaType.APPLICATION_JSON + header("Authorization", "Bearer $accessToken") + content = objectMapper.writeValueAsString(request) + } + + // then + response.andExpect { + status { isNotFound() } + } + } + } } diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/ReceiveDraftLetter.kt b/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/ReceiveDraftLetter.kt index f8c2c02..7a73ddc 100644 --- a/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/ReceiveDraftLetter.kt +++ b/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/ReceiveDraftLetter.kt @@ -11,8 +11,8 @@ class ReceiveDraftLetter( val ownerId: DomainId, var images: List, var lastUpdated: LocalDateTime = LocalDateTime.now(), - val type: ReceiveDraftLetterType, -) : BaseEntity() { + val type: ReceiveDraftLetterType, // TODO: 상속 구조를 통한 타입 구분 생각해보기 +) : BaseEntity(id) { companion object { fun default(ownerId: DomainId) = ReceiveDraftLetter( diff --git a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/adapter/ReceiveDraftLetterManagementJpaAdapter.kt b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/adapter/ReceiveDraftLetterManagementJpaAdapter.kt index 3cb07b6..aab92f4 100644 --- a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/adapter/ReceiveDraftLetterManagementJpaAdapter.kt +++ b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/adapter/ReceiveDraftLetterManagementJpaAdapter.kt @@ -1,6 +1,8 @@ package com.asap.persistence.jpa.letter.adapter +import com.asap.application.letter.exception.LetterException import com.asap.application.letter.port.out.ReceiveDraftLetterManagementPort +import com.asap.domain.common.DomainId import com.asap.domain.letter.entity.ReceiveDraftLetter import com.asap.persistence.jpa.letter.ReceiveDraftLetterMapper import com.asap.persistence.jpa.letter.repository.ReceiveDraftLetterJpaRepository @@ -15,4 +17,14 @@ class ReceiveDraftLetterManagementJpaAdapter( return receiveDraftLetterJpaRepository.save(receiveDraftLetterEntity) .let { ReceiveDraftLetterMapper.toDomain(it) } } + + override fun getDraftLetterNotNull( + draftId: DomainId, + ownerId: DomainId + ): ReceiveDraftLetter { + return receiveDraftLetterJpaRepository + .findBy(draftId.value, ownerId.value) + ?.let { ReceiveDraftLetterMapper.toDomain(it) } + ?: throw LetterException.DraftLetterNotFoundException() + } } \ No newline at end of file diff --git a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/repository/ReceiveDraftLetterJpaRepository.kt b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/repository/ReceiveDraftLetterJpaRepository.kt index 4d64f24..10f864e 100644 --- a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/repository/ReceiveDraftLetterJpaRepository.kt +++ b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/repository/ReceiveDraftLetterJpaRepository.kt @@ -2,6 +2,14 @@ package com.asap.persistence.jpa.letter.repository import com.asap.persistence.jpa.letter.entity.ReceiveDraftLetterEntity import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query interface ReceiveDraftLetterJpaRepository : JpaRepository{ + @Query(""" + SELECT rdl + FROM ReceiveDraftLetterEntity rdl + WHERE rdl.id = :id + AND rdl.ownerId = :ownerId + """) + fun findBy(id: String, ownerId: String): ReceiveDraftLetterEntity? } \ No newline at end of file