From 9c884e03cab571899ed09ce564711f37ff6cba3b Mon Sep 17 00:00:00 2001 From: whereami2048 Date: Tue, 14 Jan 2025 01:09:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=B4=88=EB=8C=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A7=8C=EB=A3=8C=20=EC=97=AC=EB=B6=80=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20CareScope=20Enum?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vacgom/api/invitation/domain/CareScope.kt | 5 ----- .../kr/co/vacgom/api/invitation/domain/InvitationCode.kt | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 src/main/kotlin/kr/co/vacgom/api/invitation/domain/CareScope.kt diff --git a/src/main/kotlin/kr/co/vacgom/api/invitation/domain/CareScope.kt b/src/main/kotlin/kr/co/vacgom/api/invitation/domain/CareScope.kt deleted file mode 100644 index 2330bbe..0000000 --- a/src/main/kotlin/kr/co/vacgom/api/invitation/domain/CareScope.kt +++ /dev/null @@ -1,5 +0,0 @@ -package kr.co.vacgom.api.invitation.domain - -enum class CareScope { - RESPECTIVE, TOTAL -} diff --git a/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt b/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt index 578dedb..548624f 100644 --- a/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt +++ b/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt @@ -7,6 +7,7 @@ data class InvitationCode( val key: String, val userId: UUID, val babyIds: List, + val isExpired: Boolean = false, val timeToLive: Long = 86400L, val createdAt: LocalDateTime = LocalDateTime.now() ) From 658c7277995bc4020145b12d20e7891c5c2e3ba1 Mon Sep 17 00:00:00 2001 From: whereami0404 Date: Tue, 14 Jan 2025 19:45:35 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=B4=88=EB=8C=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20isExpired=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vacgom/api/invitation/domain/InvitationCode.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt b/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt index 548624f..3c2561b 100644 --- a/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt +++ b/src/main/kotlin/kr/co/vacgom/api/invitation/domain/InvitationCode.kt @@ -7,7 +7,7 @@ data class InvitationCode( val key: String, val userId: UUID, val babyIds: List, - val isExpired: Boolean = false, + var isExpired: Boolean = false, val timeToLive: Long = 86400L, - val createdAt: LocalDateTime = LocalDateTime.now() + val createdAt: LocalDateTime = LocalDateTime.now(), ) From 4ef83a7723f60c995a614147e0b067bdf6510e79 Mon Sep 17 00:00:00 2001 From: whereami0404 Date: Tue, 14 Jan 2025 19:46:08 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20redis=20Repository=EC=B4=88?= =?UTF-8?q?=EB=8C=80=EC=BD=94=EB=93=9C=20=EB=A7=8C=EB=A3=8C=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EB=A1=A4=EB=B0=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/InvitationRedisRepository.kt | 19 +++++++++++++++---- .../repository/InvitationRepository.kt | 3 ++- .../repository/InvitationRepositoryAdapter.kt | 11 +++++++++-- .../kr/co/vacgom/api/redis/RedissonConfig.kt | 9 +-------- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRedisRepository.kt b/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRedisRepository.kt index 3cf9894..978db1b 100644 --- a/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRedisRepository.kt +++ b/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRedisRepository.kt @@ -18,14 +18,25 @@ class InvitationRedisRepository( .also { value -> redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS)} } - fun getAndDeleteInvitationCode(code: String): InvitationCode? { - val queryKey = INVITATION_CODE_KEY_PREFIX + code - val value = redisTemplate.opsForValue().getAndDelete(queryKey) + fun getInvitationCodeAndUpdateExpired(code: String): InvitationCode? { + val key = INVITATION_CODE_KEY_PREFIX + code + return redisTemplate.execute { getInvitationCodeAndUpdateExpired(key, true) } + } + + fun rollBackExpiredInvitationCode(code: String) { + val key = INVITATION_CODE_KEY_PREFIX + code + redisTemplate.execute { getInvitationCodeAndUpdateExpired(key, false) } + } + + private fun getInvitationCodeAndUpdateExpired(key: String, expireStatus: Boolean): InvitationCode? { + val value = redisTemplate.opsForValue().get(key) ?: return null return objectMapper.readValue(value.toString(), InvitationCode::class.java) + .apply { isExpired = expireStatus } + .also { redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(it)) } } - companion object{ + companion object { const val INVITATION_CODE_KEY_PREFIX = "invitation_code:" } } diff --git a/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepository.kt b/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepository.kt index f1e5b2c..89e4ef3 100644 --- a/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepository.kt +++ b/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepository.kt @@ -4,5 +4,6 @@ import kr.co.vacgom.api.invitation.domain.InvitationCode interface InvitationRepository { fun save(invitationCode: InvitationCode, ttl: Long) - fun getAndDeleteInvitationCode(code: String): InvitationCode? + fun getInvitationCodeAndUpdateExpired(code: String): InvitationCode + fun rollBackInvitationCodeExpired(code: String) } diff --git a/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepositoryAdapter.kt b/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepositoryAdapter.kt index a5b88fe..f6eade5 100644 --- a/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepositoryAdapter.kt +++ b/src/main/kotlin/kr/co/vacgom/api/invitation/repository/InvitationRepositoryAdapter.kt @@ -1,6 +1,8 @@ package kr.co.vacgom.api.invitation.repository +import kr.co.vacgom.api.global.exception.error.BusinessException import kr.co.vacgom.api.invitation.domain.InvitationCode +import kr.co.vacgom.api.invitation.exception.InvitationError import org.springframework.stereotype.Repository @Repository @@ -11,7 +13,12 @@ class InvitationRepositoryAdapter( invitationRedisRepository.save(invitationCode, ttl) } - override fun getAndDeleteInvitationCode(code: String): InvitationCode? { - return invitationRedisRepository.getAndDeleteInvitationCode(code) + override fun getInvitationCodeAndUpdateExpired(code: String): InvitationCode { + return invitationRedisRepository.getInvitationCodeAndUpdateExpired(code) ?: + throw BusinessException(InvitationError.INVITATION_CODE_NOT_FOUND) + } + + override fun rollBackInvitationCodeExpired(code: String) { + invitationRedisRepository.rollBackExpiredInvitationCode(code) } } diff --git a/src/main/kotlin/kr/co/vacgom/api/redis/RedissonConfig.kt b/src/main/kotlin/kr/co/vacgom/api/redis/RedissonConfig.kt index c43d3b4..360c7ee 100644 --- a/src/main/kotlin/kr/co/vacgom/api/redis/RedissonConfig.kt +++ b/src/main/kotlin/kr/co/vacgom/api/redis/RedissonConfig.kt @@ -42,14 +42,7 @@ class RedissonConfig( connectionFactory = redisConnectionFactory keySerializer = StringRedisSerializer() valueSerializer = serializer + setEnableTransactionSupport(true) } } - -// @Bean -// fun objectMapper(): ObjectMapper { -// return ObjectMapper().apply { -// registerModule(KotlinModule.Builder().build()) -// registerModule(JavaTimeModule()) -// } -// } } From 6de59a32fa31059c61f9a56bf1aef0721b5aa80e Mon Sep 17 00:00:00 2001 From: whereami0404 Date: Tue, 14 Jan 2025 19:46:44 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=EC=B4=88=EB=8C=80=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20application=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D=20=EC=8B=9C?= =?UTF-8?q?=20=EB=A0=88=EB=94=94=EC=8A=A4=20=EB=A1=A4=EB=B0=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/InvitationService.kt | 39 ++++++++++++------- .../application/InvitationServiceTest.kt | 4 +- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/kr/co/vacgom/api/invitation/application/InvitationService.kt b/src/main/kotlin/kr/co/vacgom/api/invitation/application/InvitationService.kt index 0b5ecf4..14bbf75 100644 --- a/src/main/kotlin/kr/co/vacgom/api/invitation/application/InvitationService.kt +++ b/src/main/kotlin/kr/co/vacgom/api/invitation/application/InvitationService.kt @@ -9,6 +9,7 @@ import kr.co.vacgom.api.invitation.domain.InvitationCode import kr.co.vacgom.api.invitation.exception.InvitationError import kr.co.vacgom.api.invitation.presentation.dto.InvitationDto import kr.co.vacgom.api.invitation.repository.InvitationRepository +import org.slf4j.Logger import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.util.* @@ -18,7 +19,9 @@ class InvitationService( private val invitationRepository: InvitationRepository, private val babyManagerService: BabyManagerService, private val babyQueryService: BabyQueryService, + private val logger: Logger, ) { + @Transactional fun createInvitationCodeByBabyId(userId: UUID, babyId: UUID): InvitationDto.Response.Create { val key = UuidCreator.create().toString() @@ -42,19 +45,29 @@ class InvitationService( return InvitationDto.Response.Create(invitationCode = key) } - @Transactional + @Transactional(readOnly = true) fun getBabiesByInvitationCode(code: String): List { - val invitationCode = invitationRepository.getAndDeleteInvitationCode(code) - ?: throw BusinessException(InvitationError.INVITATION_CODE_NOT_FOUND) - - return babyQueryService.getBabiesById(invitationCode.babyIds).map { - BabyDto.Response.Detail( - id = it.id, - name = it.name, - profileImg = it.profileImg, - gender = it.gender, - birthday = it.birthday, - ) - } + return runCatching { + val invitationCode = invitationRepository.getInvitationCodeAndUpdateExpired(code) + + babyQueryService.getBabiesById(invitationCode.babyIds).map { + BabyDto.Response.Detail( + id = it.id, + name = it.name, + profileImg = it.profileImg, + gender = it.gender, + birthday = it.birthday, + ) + } + }.onFailure { + if (it is BusinessException) { + if (it.errorCode == InvitationError.INVITATION_CODE_NOT_FOUND) { + throw it + } + } + + invitationRepository.rollBackInvitationCodeExpired(code) + logger.warn("Expired Invitation Code roll back to redis") + }.getOrThrow() } } diff --git a/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt b/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt index a74e6fe..6663d98 100644 --- a/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt +++ b/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt @@ -122,7 +122,7 @@ class InvitationServiceTest : DescribeSpec({ context("초대 코드를 정상적으로 조회한다면") { it("해당하는 아기 상세 정보를 반환한다.") { - every { invitationRepositoryMock.getAndDeleteInvitationCode(invitationCode.key) } returns invitationCode + every { invitationRepositoryMock.getInvitationCodeAndUpdateExpired(invitationCode.key) } returns invitationCode every { babyQueryServiceMock.getBabiesById(babies.map { it.id }) } returns babies val result = sut.getBabiesByInvitationCode(invitationCode.key) @@ -140,7 +140,7 @@ class InvitationServiceTest : DescribeSpec({ context("초대 코드가 존재하지 않는다면") { it("${InvitationError.INVITATION_CODE_NOT_FOUND} 예외가 발생한다.") { every { - invitationRepositoryMock.getAndDeleteInvitationCode(invitationCode.key) + invitationRepositoryMock.getInvitationCodeAndUpdateExpired(invitationCode.key) } throws BusinessException(InvitationError.INVITATION_CODE_NOT_FOUND) val result = shouldThrow { sut.getBabiesByInvitationCode(invitationCode.key) } From 54161db71153eb61187ed7fbe132f726bc251044 Mon Sep 17 00:00:00 2001 From: whereami0404 Date: Tue, 14 Jan 2025 20:27:11 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20Logger=20Mock=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vacgom/api/invitation/application/InvitationServiceTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt b/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt index 6663d98..5475cfc 100644 --- a/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt +++ b/src/test/kotlin/kr/co/vacgom/api/invitation/application/InvitationServiceTest.kt @@ -21,6 +21,7 @@ import kr.co.vacgom.api.invitation.exception.InvitationError import kr.co.vacgom.api.invitation.repository.InvitationRepository import kr.co.vacgom.api.user.domain.User import kr.co.vacgom.api.user.domain.enums.UserRole +import org.slf4j.Logger import java.time.LocalDate import java.util.* @@ -28,11 +29,13 @@ class InvitationServiceTest : DescribeSpec({ val invitationRepositoryMock: InvitationRepository = mockk(relaxed = true) val babyManagerServiceMock: BabyManagerService = mockk(relaxed = true) val babyQueryServiceMock: BabyQueryService = mockk(relaxed = true) + val loggerMock: Logger = mockk(relaxed = true) val sut = InvitationService( invitationRepositoryMock, babyManagerServiceMock, babyQueryServiceMock, + logger = loggerMock ) describe("초대 코드 생성 테스트") {