From 855674e0146032c6513be51216ace8908f441677 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Sun, 18 May 2025 20:30:09 +0900 Subject: [PATCH] =?UTF-8?q?ASAP-457=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EC=8B=9C=20=EB=B9=84=ED=9A=8C=EC=9B=90=20=ED=8E=B8=EC=A7=80?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `UserController.registerUser`에 비회원 편지 코드 처리 로직 추가: `addAnonymousLetter` 메서드를 호출하여 편지와 유저를 연결. - `RegisterUserRequest` DTO에 `anonymousSendLetterCode` 필드 추가. - `AddLetterUsecase`에 익명 편지를 사용자의 편지로 추가하는 `addAnonymousLetter` 메서드 및 관련 커맨드 추가. - `SendLetter` 엔터티에 `configSendId` 메서드 추가: 발신자 ID 설정 및 검증. - 관련 테스트 코드 추가 및 업데이트: 비회원 코드 처리 케이스 통합 테스트 포함. --- .../letter/port/in/AddLetterUsecase.kt | 7 ++ .../letter/service/LetterCommandService.kt | 8 ++ .../user/port/in/RegisterUserUsecase.kt | 9 +- .../user/service/UserCommandService.kt | 23 ++--- .../service/LetterCommandServiceTest.kt | 34 +++++++ .../web/user/controller/UserController.kt | 13 +++ .../web/user/dto/RegisterUserRequest.kt | 7 +- .../user/controller/UserControllerTest.kt | 7 +- .../user/UserApiIntegrationTest.kt | 98 +++++++++++++++---- .../asap/domain/letter/entity/SendLetter.kt | 10 +- 10 files changed, 173 insertions(+), 43 deletions(-) diff --git a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/AddLetterUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/AddLetterUsecase.kt index c915c149..f4cb7a8b 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/AddLetterUsecase.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/letter/port/in/AddLetterUsecase.kt @@ -5,6 +5,8 @@ interface AddLetterUsecase { fun addPhysicalLetter(command: Command.AddPhysicalLetter) + fun addAnonymousLetter(command: Command.AddAnonymousLetter) + sealed class Command { data class VerifyLetter( val letterId: String, @@ -19,5 +21,10 @@ interface AddLetterUsecase { val userId: String, val draftId: String?, ) : Command() + + data class AddAnonymousLetter( + val letterCode: String, + val userId: String, + ) : Command() } } 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 23e699b3..16646c26 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 @@ -152,6 +152,14 @@ class LetterCommandService( independentLetterManagementPort.save(independentLetter) } + override fun addAnonymousLetter(command: AddLetterUsecase.Command.AddAnonymousLetter) { + val sendLetter = sendLetterManagementPort.getLetterByCodeNotNull(command.letterCode) + val user = userManagementPort.getUserNotNull(DomainId(command.userId)) + + sendLetter.configSenderId(user.id) + sendLetterManagementPort.save(sendLetter) + } + override fun moveToSpace(command: MoveLetterUsecase.Command.ToSpace) { independentLetterManagementPort.getIndependentLetterByIdNotNull(DomainId(command.letterId)).apply { val spaceLetter = SpaceLetter.createByIndependentLetter(this, DomainId(command.spaceId)) diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt index a4bf6ca8..be0d571a 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt @@ -3,21 +3,20 @@ package com.asap.application.user.port.`in` import java.time.LocalDate interface RegisterUserUsecase { - fun registerUser(command: Command): Response - data class Command( val registerToken: String, val servicePermission: Boolean, val privatePermission: Boolean, val marketingPermission: Boolean, val birthday: LocalDate?, - val realName: String + val realName: String, ) data class Response( val accessToken: String, - val refreshToken: String + val refreshToken: String, + val userId: String, ) -} \ No newline at end of file +} diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/service/UserCommandService.kt b/Application-Module/src/main/kotlin/com/asap/application/user/service/UserCommandService.kt index b3951f32..b916cc65 100644 --- a/Application-Module/src/main/kotlin/com/asap/application/user/service/UserCommandService.kt +++ b/Application-Module/src/main/kotlin/com/asap/application/user/service/UserCommandService.kt @@ -62,21 +62,19 @@ class UserCommandService( userTokenManagementPort.saveUserToken(UserToken.create(token = refreshToken, userId = registerUser.id)) - return RegisterUserUsecase.Response(accessToken, refreshToken) + return RegisterUserUsecase.Response(accessToken, refreshToken, registerUser.id.value) } override fun delete(command: DeleteUserUsecase.Command) { - userManagementPort - .getUserNotNull(DomainId(command.userId)) - .apply { - this.delete(command.reason) - userManagementPort.save(this) - }.also { - userAuthManagementPort.getNotNull(it.id).apply { - this.delete() - userAuthManagementPort.saveUserAuth(this) - } - } + val user = userManagementPort.getUserNotNull(DomainId(command.userId)) + + user.delete(command.reason) + userManagementPort.save(user) + + val userAuth = userAuthManagementPort.getNotNull(user.id) + + userAuth.delete() + userAuthManagementPort.saveUserAuth(userAuth) } override fun executeFor(command: UpdateUserUsecase.Command.Birthday) { @@ -95,6 +93,5 @@ class UserCommandService( this.updateOnboarding() userManagementPort.save(this) } - } } 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 baf2d529..0ba460f2 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 @@ -362,6 +362,40 @@ class LetterCommandServiceTest : } } + + given("익명 편지를 사용자의 편지로 추가할 때") { + val letterCode = "letter-code" + val userId = "user-id" + val command = AddLetterUsecase.Command.AddAnonymousLetter( + letterCode = letterCode, + userId = userId, + ) + + // Create an anonymous letter using SendLetter.createAnonymous + val content = LetterContent( + content = "content", + templateType = 1, + images = mutableListOf("image1", "image2"), + ) + val sendLetter = SendLetter.createAnonymous( + content = content, + receiverName = "receiverName", + letterCode = letterCode, + ) + + every { + mockSendLetterManagementPort.getLetterByCodeNotNull(letterCode) + } returns sendLetter + + `when`("익명 편지를 사용자의 편지로 추가하면") { + letterCommandService.addAnonymousLetter(command) + + then("편지의 발신자 ID가 설정되고 저장되어야 한다") { + verify { mockSendLetterManagementPort.save(sendLetter) } + } + } + } + given("보낸 편지 삭제 요청이 들어올 때") { val userId = "user-id" val sendLetters = diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/controller/UserController.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/controller/UserController.kt index bb413457..5a9608ec 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/controller/UserController.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/controller/UserController.kt @@ -1,5 +1,6 @@ package com.asap.bootstrap.web.user.controller +import com.asap.application.letter.port.`in`.AddLetterUsecase import com.asap.application.user.port.`in`.* import com.asap.bootstrap.web.user.api.UserApi import com.asap.bootstrap.web.user.dto.* @@ -12,6 +13,7 @@ class UserController( private val deleteUserUsecase: DeleteUserUsecase, private val getUserInfoUsecase: GetUserInfoUsecase, private val updateUserUsecase: UpdateUserUsecase, + private val addLetterUsecase: AddLetterUsecase, ) : UserApi { override fun registerUser(request: RegisterUserRequest): RegisterUserResponse { val response = @@ -25,6 +27,17 @@ class UserController( request.realName, ), ) + + // Handle anonymous letter code if it exists + request.anonymousSendLetterCode?.let { letterCode -> + addLetterUsecase.addAnonymousLetter( + AddLetterUsecase.Command.AddAnonymousLetter( + letterCode = letterCode, + userId = response.userId, + ), + ) + } + return RegisterUserResponse(response.accessToken, response.refreshToken) } diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/dto/RegisterUserRequest.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/dto/RegisterUserRequest.kt index f47ce4c7..4fe39b36 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/dto/RegisterUserRequest.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/web/user/dto/RegisterUserRequest.kt @@ -16,6 +16,7 @@ data class RegisterUserRequest( @Schema(description = "생년 월일, yyyy-MM-dd, 값이 안넘어올 수 있음") val birthday: LocalDate?, @Schema(description = "실명") - val realName: String -) { -} \ No newline at end of file + val realName: String, + @Schema(description = "비회원 상태로 전송한 편지의 코드") + val anonymousSendLetterCode: String?, +) diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/user/controller/UserControllerTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/user/controller/UserControllerTest.kt index 5bf6b41a..a8326dd2 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/user/controller/UserControllerTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/acceptance/user/controller/UserControllerTest.kt @@ -40,7 +40,7 @@ class UserControllerTest : AcceptanceSupporter() { @Test fun registerUserTest() { // given - val request = RegisterUserRequest("register", true, true, true, LocalDate.now(), "realName") + val request = RegisterUserRequest("register", true, true, true, LocalDate.now(), "realName", null) val command = RegisterUserUsecase.Command( request.registerToken, @@ -53,8 +53,9 @@ class UserControllerTest : AcceptanceSupporter() { given(registerUserUsecase.registerUser(command)).willReturn( RegisterUserUsecase.Response( "accessToken", - "refreshToken" - ) + "refreshToken", + "userId", + ), ) // when val response = diff --git a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt index 8a68e0ac..6c327024 100644 --- a/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt +++ b/Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt @@ -1,5 +1,7 @@ package com.asap.bootstrap.integration.user +import com.asap.application.letter.LetterMockManager +import com.asap.application.letter.port.out.SendLetterManagementPort import com.asap.application.user.exception.UserException import com.asap.application.user.port.out.UserManagementPort import com.asap.bootstrap.IntegrationSupporter @@ -8,8 +10,12 @@ import com.asap.bootstrap.web.user.dto.RegisterUserRequest import com.asap.bootstrap.web.user.dto.UnregisterUserRequest import com.asap.bootstrap.web.user.dto.UpdateBirthdayRequest import com.asap.domain.common.DomainId +import com.asap.domain.letter.entity.SendLetter +import com.asap.domain.letter.enums.LetterStatus +import com.asap.domain.letter.vo.LetterContent import io.kotest.matchers.comparables.shouldBeGreaterThan import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -21,13 +27,15 @@ import org.springframework.test.web.servlet.put import java.time.LocalDate class UserApiIntegrationTest( - private val userManagementPort: UserManagementPort + private val userManagementPort: UserManagementPort, + private val letterMockManager: LetterMockManager, + private val sendLetterManagementPort: SendLetterManagementPort, ) : IntegrationSupporter() { @Test fun registerUserSuccessTest() { // given val registerToken = jwtMockManager.generateRegisterToken() - val request = RegisterUserRequest(registerToken, true, true, true, LocalDate.now(), "realName") + val request = RegisterUserRequest(registerToken, true, true, true, LocalDate.now(), "realName", null) // when val response = mockMvc.post("/api/v1/users") { @@ -54,7 +62,7 @@ class UserApiIntegrationTest( fun registerUserInvalidTest_with_DuplicateUser() { // given val duplicateRegisterToken = jwtMockManager.generateRegisterToken() - val request = RegisterUserRequest(duplicateRegisterToken, true, true, true, LocalDate.now(), "realName") + val request = RegisterUserRequest(duplicateRegisterToken, true, true, true, LocalDate.now(), "realName", null) mockMvc.post("/api/v1/users") { contentType = MediaType.APPLICATION_JSON content = objectMapper.writeValueAsString(request) @@ -76,7 +84,7 @@ class UserApiIntegrationTest( // given val userId = userMockManager.settingUser() val registerToken = jwtMockManager.generateInvalidToken() - val request = RegisterUserRequest(registerToken, true, true, true, LocalDate.now(), "realName") + val request = RegisterUserRequest(registerToken, true, true, true, LocalDate.now(), "realName", null) // when val response = mockMvc.post("/api/v1/users") { @@ -97,7 +105,7 @@ class UserApiIntegrationTest( fun registerUserInvalidTest_with_NonSavedRegisterToken() { // given val registerToken = jwtMockManager.generateInvalidToken() - val request = RegisterUserRequest(registerToken, true, true, true, LocalDate.now(), "realName") + val request = RegisterUserRequest(registerToken, true, true, true, LocalDate.now(), "realName", null) // when val response = mockMvc.post("/api/v1/users") { @@ -119,7 +127,7 @@ class UserApiIntegrationTest( fun registerUserInvalidTest_with_InvalidServicePermission() { // given val registerToken = jwtMockManager.generateRegisterToken() - val request = RegisterUserRequest(registerToken, false, true, true, LocalDate.now(), "realName") + val request = RegisterUserRequest(registerToken, false, true, true, LocalDate.now(), "realName", null) // when val response = mockMvc.post("/api/v1/users") { @@ -136,7 +144,7 @@ class UserApiIntegrationTest( fun registerUserInvalidTest_with_InvalidPrivatePermission() { // given val registerToken = jwtMockManager.generateRegisterToken() - val request = RegisterUserRequest(registerToken, true, false, true, LocalDate.now(), "realName") + val request = RegisterUserRequest(registerToken, true, false, true, LocalDate.now(), "realName", null) // when val response = mockMvc.post("/api/v1/users") { @@ -194,7 +202,7 @@ class UserApiIntegrationTest( @Nested @DisplayName("deleteUser") - inner class DeleteUser{ + inner class DeleteUser { @Test fun deleteUser() { // given @@ -216,13 +224,12 @@ class UserApiIntegrationTest( } @Test - fun deleteUser_with_reason(){ + fun deleteUser_with_reason() { // given val userId = userMockManager.settingUser() userMockManager.settingUserAuth(userId = userId) val accessToken = jwtMockManager.generateAccessToken(userId) val request = UnregisterUserRequest("reason") - // when val response = mockMvc.delete("/api/v1/users") { @@ -230,7 +237,6 @@ class UserApiIntegrationTest( content = objectMapper.writeValueAsString(request) header("Authorization", "Bearer $accessToken") } - // then response.andExpect { status { isOk() } @@ -246,14 +252,12 @@ class UserApiIntegrationTest( val userId = userMockManager.settingUser() userMockManager.settingUserAuth(userId = userId) val accessToken = jwtMockManager.generateAccessToken(userId) - // when val response = mockMvc.get("/api/v1/users/info/me") { contentType = MediaType.APPLICATION_JSON header("Authorization", "Bearer $accessToken") } - // then response.andExpect { status { isOk() } @@ -282,7 +286,6 @@ class UserApiIntegrationTest( val userId = userMockManager.settingUser() val accessToken = jwtMockManager.generateAccessToken(userId) val request = UpdateBirthdayRequest(LocalDate.now()) - // when val response = mockMvc.put("/api/v1/users/info/me/birthday") { @@ -290,7 +293,6 @@ class UserApiIntegrationTest( content = objectMapper.writeValueAsString(request) header("Authorization", "Bearer $accessToken") } - // then response.andExpect { status { isOk() } @@ -303,16 +305,76 @@ class UserApiIntegrationTest( val userId = userMockManager.settingUser() userMockManager.settingUserAuth(userId = userId) val accessToken = jwtMockManager.generateAccessToken(userId) - + val beforeUpdatedAt = userManagementPort.getUserNotNull(DomainId(userId)).updatedAt // when mockMvc.delete("/api/v1/users") { contentType = MediaType.APPLICATION_JSON header("Authorization", "Bearer $accessToken") } + // then + val afterUpdatedAt = userManagementPort.getUserNotNull(DomainId(userId)).updatedAt + afterUpdatedAt shouldBeGreaterThan beforeUpdatedAt + } + @Test + fun registerUserWithAnonymousSendLetterCodeTest() { + // given + // Create an anonymous letter + val letterContent = + LetterContent( + content = "anonymous letter content", + templateType = 1, + images = mutableListOf("image1", "image2"), + ) + val receiverName = "testReceiver" + val anonymousSendLetter = + SendLetter.createAnonymous( + content = letterContent, + receiverName = receiverName, + letterCode = "test-letter-code", + status = LetterStatus.SENDING, + ) + sendLetterManagementPort.save(anonymousSendLetter) + + val letterCode = anonymousSendLetter.letterCode!! + + // Register a user with the anonymous letter code + val registerToken = jwtMockManager.generateRegisterToken() + val request = + RegisterUserRequest( + registerToken = registerToken, + servicePermission = true, + privatePermission = true, + marketingPermission = true, + birthday = LocalDate.now(), + realName = receiverName, + anonymousSendLetterCode = letterCode, + ) + + // when + val response = + mockMvc.post("/api/v1/users") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } // then - val user = userManagementPort.getUserNotNull(DomainId(userId)) - user.updatedAt shouldBeGreaterThan user.createdAt + response.andExpect { + status { isOk() } + jsonPath("$.accessToken") { + exists() + isString() + isNotEmpty() + } + jsonPath("$.refreshToken") { + exists() + isString() + isNotEmpty() + } + } + + // Verify that the letter is properly associated with the user + val updatedLetter = sendLetterManagementPort.getLetterByCodeNotNull(letterCode) + updatedLetter.senderId shouldNotBe null } } 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 1796478b..0fdf191e 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? = null, + var senderId: DomainId?, var receiverName: String, var letterCode: String?, var status: LetterStatus, @@ -90,4 +90,12 @@ class SendLetter( this.receiverId = null updateTime() } + + fun configSenderId(senderId: DomainId) { + check(this.senderId == null) { + "SenderId is already set" + } + this.senderId = senderId + updateTime() + } }