diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/GetVerifiedLetterUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/GetVerifiedLetterUsecase.kt index da126c9d..f9ec9932 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/GetVerifiedLetterUsecase.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/GetVerifiedLetterUsecase.kt @@ -3,23 +3,18 @@ package com.asap.application.letter.port.`in` import java.time.LocalDate interface GetVerifiedLetterUsecase { - - fun get( - query: Query - ): Response + fun get(query: Query): Response data class Query( val letterId: String, - val userId: String + val userId: String, ) data class Response( - val senderName: String, + val senderName: String?, val content: String, val sendDate: LocalDate, val templateType: Int, - val images: List + val images: List, ) - - -} \ No newline at end of file +} diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/SendLetterUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/SendLetterUsecase.kt index 9bca7b07..ede1b7dd 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/SendLetterUsecase.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/SendLetterUsecase.kt @@ -1,10 +1,9 @@ package com.asap.application.letter.port.`in` interface SendLetterUsecase { + fun send(command: Command): Response - fun send( - command: Command - ): Response + fun sendAnonymous(command: AnonymousCommand): Response data class Command( val receiverName: String, @@ -12,10 +11,17 @@ interface SendLetterUsecase { val images: List, val templateType: Int, val draftId: String?, - val userId: String + val userId: String, + ) + + data class AnonymousCommand( + val receiverName: String, + val content: String, + val images: List, + val templateType: Int, ) data class Response( - val letterCode: String + val letterCode: String, ) -} \ No newline at end of file +} diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterCommandService.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterCommandService.kt index 3fa993d6..23e699b3 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterCommandService.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterCommandService.kt @@ -56,6 +56,27 @@ class LetterCommandService( return SendLetterUsecase.Response(letterCode = sendLetter.letterCode!!) } + override fun sendAnonymous(command: SendLetterUsecase.AnonymousCommand): SendLetterUsecase.Response { + val sendLetter = + SendLetter.createAnonymous( + receiverName = command.receiverName, + content = + LetterContent( + content = command.content, + templateType = command.templateType, + images = command.images.toMutableList(), + ), + letterCode = + letterCodeGenerator.generateCode( + content = command.content, + ), + ) + + sendLetterManagementPort.save(sendLetter) + + return SendLetterUsecase.Response(letterCode = sendLetter.letterCode!!) + } + override fun verify(command: VerifyLetterAccessibleUsecase.Command): VerifyLetterAccessibleUsecase.Response { if (sendLetterManagementPort.verifiedLetter(DomainId(command.userId), command.letterCode)) { val sendLetter = @@ -67,15 +88,15 @@ class LetterCommandService( } val sendLetter = sendLetterManagementPort.getLetterByCodeNotNull(command.letterCode) - sendLetter - .isSameReceiver { - userManagementPort.getUserNotNull(DomainId(command.userId)) - }.takeIf { it } - ?.let { - sendLetter.readLetter(DomainId(command.userId)) - sendLetterManagementPort.save(sendLetter) - return VerifyLetterAccessibleUsecase.Response(letterId = sendLetter.id.value) - } ?: throw LetterException.InvalidLetterAccessException() + val receiver = userManagementPort.getUserNotNull(DomainId(command.userId)) + + if (sendLetter.isSameReceiver(receiver)) { + sendLetter.readLetter(DomainId(command.userId)) + sendLetterManagementPort.save(sendLetter) + return VerifyLetterAccessibleUsecase.Response(letterId = sendLetter.id.value) + } + + throw LetterException.InvalidLetterAccessException() } override fun addVerifiedLetter(command: AddLetterUsecase.Command.VerifyLetter) { @@ -89,7 +110,10 @@ class LetterCommandService( sender = SenderInfo( senderId = sendLetter.senderId, - senderName = userManagementPort.getUserNotNull(sendLetter.senderId).username, + senderName = + sendLetter.senderId + ?.let { userManagementPort.getUserNotNull(it).username } + .orEmpty(), ), receiver = ReceiverInfo( diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterQueryService.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterQueryService.kt index 5d473561..44bf6df6 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterQueryService.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterQueryService.kt @@ -32,9 +32,9 @@ class LetterQueryService( receiverId = DomainId(query.userId), letterId = DomainId(query.letterId), ).also { - val sender = userManagementPort.getUserNotNull(it.senderId) + val sender = it.senderId?.let { userManagementPort.getUserNotNull(it) } return GetVerifiedLetterUsecase.Response( - senderName = sender.username, + senderName = sender?.username, content = it.content.content, sendDate = it.createdDate, templateType = it.content.templateType, diff --git a/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterCommandServiceTest.kt b/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterCommandServiceTest.kt index 7f866525..baf2d529 100644 --- a/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterCommandServiceTest.kt +++ b/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterCommandServiceTest.kt @@ -57,6 +57,26 @@ class LetterCommandServiceTest : } } + given("익명 편지 전송 요청이 들어올 때") { + val command = + SendLetterUsecase.AnonymousCommand( + receiverName = "receiver-name", + content = "content", + images = emptyList(), + templateType = 1, + ) + `when`("익명 편지 전송 요청을 처리하면") { + val response = letterCommandService.sendAnonymous(command) + then("편지 코드가 생성되고, 편지가 저장되어야 한다") { + response.letterCode shouldNotBeNull { + this.isNotBlank() + this.isNotEmpty() + } + verify { mockSendLetterManagementPort.save(any()) } + } + } + } + given("편지 검증 시에") { val letterCode = "letter-code" val mockUser = UserFixture.createUser(username = "receiver-name") diff --git a/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterQueryServiceTest.kt b/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterQueryServiceTest.kt index 18e67b7c..6baa76b6 100644 --- a/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterQueryServiceTest.kt +++ b/Application-Module/src/test/kotlin/com/asap/application/letter/service/LetterQueryServiceTest.kt @@ -42,7 +42,7 @@ class LetterQueryServiceTest : userId = user.id.value, ) val mockSendLetter = LetterFixture.generateSendLetter(user.id) - val mockSender = UserFixture.createUser(mockSendLetter.senderId, "sender-name") + val mockSender = UserFixture.createUser(mockSendLetter.senderId!!, "sender-name") every { mockSendLetterManagementPort.getReadLetterNotNull( receiverId = DomainId(query.userId), @@ -96,9 +96,10 @@ class LetterQueryServiceTest : letterId = "letter-id", userId = "user-id", ) - val space = SpaceFixture.createSpace( - userId = DomainId(query.userId), - ) + val space = + SpaceFixture.createSpace( + userId = DomainId(query.userId), + ) val spaceLetter = LetterFixture.generateSpaceLetter(receiverId = DomainId(query.userId), spaceId = space.id) val prevSpaceLetter = LetterFixture.generateSpaceLetter(receiverId = DomainId(query.userId), spaceId = space.id) diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/LetterApi.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/LetterApi.kt index 887fbe7c..4167d774 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/LetterApi.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/api/LetterApi.kt @@ -2,20 +2,7 @@ package com.asap.bootstrap.web.letter.api import com.asap.bootstrap.common.exception.ExceptionResponse import com.asap.bootstrap.common.security.annotation.AccessUser -import com.asap.bootstrap.web.letter.dto.AddPhysicalLetterRequest -import com.asap.bootstrap.web.letter.dto.AddVerifiedLetterRequest -import com.asap.bootstrap.web.letter.dto.AllLetterCountResponse -import com.asap.bootstrap.web.letter.dto.DeleteSendLettersRequest -import com.asap.bootstrap.web.letter.dto.GetIndependentLetterDetailResponse -import com.asap.bootstrap.web.letter.dto.GetIndependentLetterSimpleInfo -import com.asap.bootstrap.web.letter.dto.LetterVerifyRequest -import com.asap.bootstrap.web.letter.dto.LetterVerifyResponse -import com.asap.bootstrap.web.letter.dto.ModifyLetterRequest -import com.asap.bootstrap.web.letter.dto.SendLetterDetailResponse -import com.asap.bootstrap.web.letter.dto.SendLetterHistoryResponse -import com.asap.bootstrap.web.letter.dto.SendLetterRequest -import com.asap.bootstrap.web.letter.dto.SendLetterResponse -import com.asap.bootstrap.web.letter.dto.VerifiedLetterInfoResponse +import com.asap.bootstrap.web.letter.dto.* import com.asap.common.page.ListResponse import com.asap.common.page.SliceResponse import io.swagger.v3.oas.annotations.Operation @@ -317,4 +304,28 @@ interface LetterApi { @RequestBody request: DeleteSendLettersRequest, @AccessUser userId: String, ) + + @Operation(summary = "비회원 편지 쓰기") + @PostMapping("/anonymous/send") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "비회원 편지 전송 성공", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = SendLetterResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "4XX", + description = "비회원 편지 전송 실패", + ), + ], + ) + fun sendAnonymousLetter( + @RequestBody request: AnonymousSendLetterRequest, + ): SendLetterResponse } diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/LetterController.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/LetterController.kt index 3019be54..3496ba7e 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/LetterController.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/controller/LetterController.kt @@ -265,4 +265,19 @@ class LetterController( ), ) } + + override fun sendAnonymousLetter(request: AnonymousSendLetterRequest): SendLetterResponse { + val response = + sendLetterUsecase.sendAnonymous( + SendLetterUsecase.AnonymousCommand( + receiverName = request.receiverName, + content = request.content, + images = request.images, + templateType = request.templateType, + ), + ) + return SendLetterResponse( + letterCode = response.letterCode, + ) + } } diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/AnonymousSendLetterRequest.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/AnonymousSendLetterRequest.kt new file mode 100644 index 00000000..147bf262 --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/AnonymousSendLetterRequest.kt @@ -0,0 +1,8 @@ +package com.asap.bootstrap.web.letter.dto + +data class AnonymousSendLetterRequest( + val receiverName: String, + val content: String, + val images: List, + val templateType: Int, +) diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/VerifiedLetterInfoResponse.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/VerifiedLetterInfoResponse.kt index e8f74351..2fe53fec 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/VerifiedLetterInfoResponse.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/dto/VerifiedLetterInfoResponse.kt @@ -3,10 +3,9 @@ package com.asap.bootstrap.web.letter.dto import java.time.LocalDate data class VerifiedLetterInfoResponse( - val senderName: String, + val senderName: String?, val content: String, val date: LocalDate, val templateType: Int, - val images: List -) { -} \ No newline at end of file + val images: List, +) diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/handler/DraftLetterEventHandler.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/handler/DraftLetterEventHandler.kt index 670b4955..ccf79ebc 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/handler/DraftLetterEventHandler.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/letter/handler/DraftLetterEventHandler.kt @@ -16,7 +16,7 @@ class DraftLetterEventHandler( event.draftId?.let { removeDraftLetterUsecase.deleteBy( RemoveDraftLetterUsecase.Command.Send( - userId = event.sendLetter.senderId.value, + userId = event.sendLetter.senderId!!.value, draftId = it, ), ) diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/LetterControllerTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/LetterControllerTest.kt index 68fb4b42..6a6ec16a 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/LetterControllerTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/letter/controller/LetterControllerTest.kt @@ -589,4 +589,41 @@ class LetterControllerTest : LetterAcceptanceSupporter() { } } } + @Test + fun sendAnonymousLetter() { + // given + val request = + AnonymousSendLetterRequest( + receiverName = "receiverName", + content = "content", + images = listOf("images"), + templateType = 1, + ) + BDDMockito + .given( + sendLetterUsecase.sendAnonymous( + SendLetterUsecase.AnonymousCommand( + receiverName = request.receiverName, + content = request.content, + images = request.images, + templateType = request.templateType, + ), + ), + ).willReturn(SendLetterUsecase.Response("letterCode")) + // when + val response = + mockMvc.post("/api/v1/letters/anonymous/send") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + // then + response.andExpect { + status { isOk() } + jsonPath("$.letterCode") { + exists() + isString() + isNotEmpty() + } + } + } } diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/LetterApiIntegrationTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/LetterApiIntegrationTest.kt index 058b862a..901324c2 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/LetterApiIntegrationTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/letter/LetterApiIntegrationTest.kt @@ -1081,4 +1081,32 @@ class LetterApiIntegrationTest : IntegrationSupporter() { } } } + + @Test + @DisplayName("비회원 편지 쓰기") + fun sendAnonymousLetter() { + // given + val request = + AnonymousSendLetterRequest( + receiverName = "receiverName", + content = "content", + images = listOf("images"), + templateType = 1, + ) + // when + val response = + mockMvc.post("/api/v1/letters/anonymous/send") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + // then + response.andExpect { + status { isOk() } + jsonPath("$.letterCode") { + exists() + isString() + isNotEmpty() + } + } + } } diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/SendLetter.kt b/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/SendLetter.kt index 4ff0ec91..1796478b 100644 --- a/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/SendLetter.kt +++ b/Domain-Module/src/main/kotlin/com/asap/domain/letter/entity/SendLetter.kt @@ -12,7 +12,7 @@ import java.time.LocalDateTime class SendLetter( id: DomainId, val content: LetterContent, - val senderId: DomainId, + val senderId: DomainId? = null, var receiverName: String, var letterCode: String?, var status: LetterStatus, @@ -46,13 +46,31 @@ class SendLetter( ).also { it.registerEvent(SendLetterEvent.SendLetterCreatedEvent(it, draftId?.value)) } - } - fun isSameReceiver(receiver: () -> User): Boolean { - val receiverUser = receiver() - return receiverName == receiverUser.username && (receiverId == null || receiverId == receiverUser.id) + fun createAnonymous( + content: LetterContent, + receiverName: String, + letterCode: String?, + status: LetterStatus = LetterStatus.SENDING, + receiverId: DomainId? = null, + createdAt: LocalDateTime = LocalDateTime.now(), + updatedAt: LocalDateTime = LocalDateTime.now(), + ): SendLetter = + SendLetter( + id = DomainId.generate(), + content = content, + senderId = null, + receiverName = receiverName, + letterCode = letterCode, + status = status, + receiverId = receiverId, + createdAt = createdAt, + updatedAt = updatedAt, + ) } + fun isSameReceiver(receiver: User): Boolean = receiverName == receiver.username && (receiverId == null || receiverId == receiver.id) + fun readLetter(receiverId: DomainId) { this.receiverId = receiverId this.status = LetterStatus.READ diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/letter/service/LetterCodeGenerator.kt b/Domain-Module/src/main/kotlin/com/asap/domain/letter/service/LetterCodeGenerator.kt index 9d942001..bb91ebaa 100644 --- a/Domain-Module/src/main/kotlin/com/asap/domain/letter/service/LetterCodeGenerator.kt +++ b/Domain-Module/src/main/kotlin/com/asap/domain/letter/service/LetterCodeGenerator.kt @@ -4,10 +4,9 @@ import java.security.MessageDigest import java.util.* class LetterCodeGenerator { - fun generateCode( content: String, - ownerId: String + ownerId: String = UUID.randomUUID().toString(), ): String { val salt = UUID.randomUUID().toString() val input = content + ownerId + salt @@ -22,4 +21,4 @@ class LetterCodeGenerator { val letterCode = hexString.toString() return letterCode } -} \ No newline at end of file +} diff --git a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/SendLetterMapper.kt b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/SendLetterMapper.kt index 38087c43..0fa655bb 100644 --- a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/SendLetterMapper.kt +++ b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/SendLetterMapper.kt @@ -17,7 +17,7 @@ object SendLetterMapper { ), receiverName = sendLetterEntity.receiverName, letterCode = sendLetterEntity.letterCode ?: "", - senderId = DomainId(sendLetterEntity.senderId), + senderId = sendLetterEntity.senderId?.let { DomainId(it) }, receiverId = sendLetterEntity.receiverId?.let { DomainId(it) }, status = sendLetterEntity.letterStatus, createdAt = sendLetterEntity.createdAt, @@ -31,7 +31,7 @@ object SendLetterMapper { images = sendLetter.content.images, templateType = sendLetter.content.templateType, receiverName = sendLetter.receiverName, - senderId = sendLetter.senderId.value, + senderId = sendLetter.senderId?.value, letterCode = sendLetter.letterCode, receiverId = sendLetter.receiverId?.value, letterStatus = sendLetter.status, diff --git a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/entity/SendLetterEntity.kt b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/entity/SendLetterEntity.kt index 1f258e8e..8fe740ea 100644 --- a/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/entity/SendLetterEntity.kt +++ b/Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/entity/SendLetterEntity.kt @@ -26,7 +26,7 @@ class SendLetterEntity( content: String, images: List, templateType: Int, - senderId: String, + senderId: String?, letterCode: String?, receiverId: String?, letterStatus: LetterStatus, @@ -49,17 +49,16 @@ class SendLetterEntity( var images: List = images var templateType: Int = templateType - @Column(name = "sender_id", nullable = false) - var senderId: String = senderId + @Column(name = "sender_id") + var senderId: String? = senderId @ManyToOne(fetch = FetchType.LAZY) @JoinColumn( name = "sender_id", - nullable = false, insertable = false, updatable = false, ) - lateinit var sender: UserEntity + var sender: UserEntity? = null @Column(name = "receiver_id") var receiverId: String? = receiverId diff --git a/Infrastructure-Module/Persistence/src/main/resources/db/V1_20__alter_send_letter_sender_id_to_nullable.sql b/Infrastructure-Module/Persistence/src/main/resources/db/V1_20__alter_send_letter_sender_id_to_nullable.sql new file mode 100644 index 00000000..921fa3ce --- /dev/null +++ b/Infrastructure-Module/Persistence/src/main/resources/db/V1_20__alter_send_letter_sender_id_to_nullable.sql @@ -0,0 +1 @@ +ALTER TABLE send_letter MODIFY COLUMN sender_id VARCHAR(255) NULL; \ No newline at end of file