From 2c9f34fae41144afc77317c396b4521c36d644cd Mon Sep 17 00:00:00 2001 From: Jiyong Jung Date: Thu, 28 May 2026 21:37:32 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=95=9C=20=EB=AA=85=EC=9D=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=85=90=EC=9D=BC=EC=9D=84=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=ED=96=88=EC=9C=BC=EB=82=98,=20=EB=B0=98=EB=8C=80=EC=AA=BD?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EB=85=90=EC=9D=BC=EC=9D=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onboarding/OnboardingService.kt | 21 ++- .../onboarding/OnboardingServiceTest.kt | 144 ++++++++++++++++++ .../domain/onboarding/model/Onboarding.kt | 18 ++- 3 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 love/application/src/test/kotlin/com/yapp/love/application/onboarding/OnboardingServiceTest.kt diff --git a/love/application/src/main/kotlin/com/yapp/love/application/onboarding/OnboardingService.kt b/love/application/src/main/kotlin/com/yapp/love/application/onboarding/OnboardingService.kt index ef5e979..22bbfb7 100644 --- a/love/application/src/main/kotlin/com/yapp/love/application/onboarding/OnboardingService.kt +++ b/love/application/src/main/kotlin/com/yapp/love/application/onboarding/OnboardingService.kt @@ -26,11 +26,10 @@ class OnboardingService( private val userAdditionInfoRepository: UserAdditionInfoRepository, private val notificationEventPublisher: ApplicationEventPublisher, ) { - @Transactional fun getOnboardingStatus(userId: Long): OnboardingStatus { return onboardingInfoRepository.findByUserId(userId)?.status - ?:onboardingInfoRepository.save(UserOnboardingInfo.create(userId)).status + ?: onboardingInfoRepository.save(UserOnboardingInfo.create(userId)).status } @Transactional @@ -52,7 +51,7 @@ class OnboardingService( user1Id = inviteCodes.creatorId, user2Id = usedUserId, inviteCodeId = inviteCodes.id!!, - ) + ), ) val coupleInfo = coupleInfoRepository.findByUserId(usedUserId) @@ -106,7 +105,21 @@ class OnboardingService( coupleInfo.setAnniversary(anniversaryDate) coupleInfoRepository.save(coupleInfo) - updateOnboardingStatus(userId, coupleInfo, OnboardingStatus.ANNIVERSARY_SETUP) + completeAnniversarySetupForCouple(coupleInfo) + } + + private fun completeAnniversarySetupForCouple(coupleInfo: CoupleInfo) { + listOf(coupleInfo.user1Id, coupleInfo.user2Id) + .forEach { userId -> + completeAnniversarySetupIfReady(userId, coupleInfo) + } + } + + private fun completeAnniversarySetupIfReady(userId: Long, coupleInfo: CoupleInfo) { + val onboardingInfo = onboardingInfoRepository.findByUserId(userId) ?: return + if (!onboardingInfo.completeAnniversarySetupIfReady(coupleInfo)) return + + onboardingInfoRepository.save(onboardingInfo) } private fun updateOnboardingStatus(userId: Long, coupleInfo: CoupleInfo, expectedStatus: OnboardingStatus? = null) { diff --git a/love/application/src/test/kotlin/com/yapp/love/application/onboarding/OnboardingServiceTest.kt b/love/application/src/test/kotlin/com/yapp/love/application/onboarding/OnboardingServiceTest.kt new file mode 100644 index 0000000..684e431 --- /dev/null +++ b/love/application/src/test/kotlin/com/yapp/love/application/onboarding/OnboardingServiceTest.kt @@ -0,0 +1,144 @@ +package com.yapp.love.application.onboarding + +import com.yapp.love.domain.couple.CoupleInfoRepository +import com.yapp.love.domain.couple.model.CoupleInfo +import com.yapp.love.domain.onboarding.InviteCodeRepository +import com.yapp.love.domain.onboarding.OnboardingInfoRepository +import com.yapp.love.domain.onboarding.model.OnboardingStatus +import com.yapp.love.domain.onboarding.model.UserOnboardingInfo +import com.yapp.love.domain.user.UserAdditionInfoRepository +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import org.springframework.context.ApplicationEventPublisher +import java.time.LocalDate + +class OnboardingServiceTest : DescribeSpec({ + + val inviteCodeRepository = mockk() + val coupleInfoRepository = mockk() + val userAdditionInfoRepository = mockk() + val eventPublisher = mockk(relaxed = true) + + val userAId = 1L + val userBId = 2L + val anniversaryDate = LocalDate.of(2025, 1, 1) + + fun createService(onboardingInfoRepository: OnboardingInfoRepository) = + OnboardingService( + onboardingInfoRepository = onboardingInfoRepository, + inviteCodeRepository = inviteCodeRepository, + coupleInfoRepository = coupleInfoRepository, + userAdditionInfoRepository = userAdditionInfoRepository, + notificationEventPublisher = eventPublisher, + ) + + beforeEach { + clearAllMocks() + every { coupleInfoRepository.save(any()) } answers { firstArg() } + every { userAdditionInfoRepository.save(any()) } answers { firstArg() } + } + + describe("setAnniversary") { + it("한 명이 기념일을 설정하면 ANNIVERSARY_SETUP 상태인 양쪽 온보딩이 완료된다") { + val coupleInfo = createCoupleInfo() + val userAOnboarding = createOnboardingInfo(userAId, OnboardingStatus.ANNIVERSARY_SETUP) + val userBOnboarding = createOnboardingInfo(userBId, OnboardingStatus.ANNIVERSARY_SETUP) + val onboardingInfoRepository = FakeOnboardingInfoRepository(userAOnboarding, userBOnboarding) + val service = createService(onboardingInfoRepository) + every { coupleInfoRepository.findByUserId(userAId) } returns coupleInfo + + service.setAnniversary(userAId, anniversaryDate) + + coupleInfo.anniversaryDate shouldBe anniversaryDate + userAOnboarding.status shouldBe OnboardingStatus.COMPLETED + userBOnboarding.status shouldBe OnboardingStatus.COMPLETED + onboardingInfoRepository.savedUserIds shouldBe listOf(userAId, userBId) + } + + it("상대가 아직 ANNIVERSARY_SETUP이 아니면 기념일 설정 시 강제로 완료하지 않는다") { + val coupleInfo = createCoupleInfo() + val userAOnboarding = createOnboardingInfo(userAId, OnboardingStatus.ANNIVERSARY_SETUP) + val userBOnboarding = createOnboardingInfo(userBId, OnboardingStatus.PROFILE_SETUP) + val onboardingInfoRepository = FakeOnboardingInfoRepository(userAOnboarding, userBOnboarding) + val service = createService(onboardingInfoRepository) + every { coupleInfoRepository.findByUserId(userAId) } returns coupleInfo + + service.setAnniversary(userAId, anniversaryDate) + + userAOnboarding.status shouldBe OnboardingStatus.COMPLETED + userBOnboarding.status shouldBe OnboardingStatus.PROFILE_SETUP + onboardingInfoRepository.savedUserIds shouldBe listOf(userAId) + } + + it("이미 완료된 사용자는 스킵하여 반복 기념일 설정이 예외를 발생시키지 않는다") { + val coupleInfo = createCoupleInfo() + val completedOnboarding = createOnboardingInfo(userAId, OnboardingStatus.COMPLETED) + val waitingOnboarding = createOnboardingInfo(userBId, OnboardingStatus.ANNIVERSARY_SETUP) + val onboardingInfoRepository = FakeOnboardingInfoRepository(completedOnboarding, waitingOnboarding) + val service = createService(onboardingInfoRepository) + every { coupleInfoRepository.findByUserId(userAId) } returns coupleInfo + + service.setAnniversary(userAId, anniversaryDate) + + completedOnboarding.status shouldBe OnboardingStatus.COMPLETED + waitingOnboarding.status shouldBe OnboardingStatus.COMPLETED + onboardingInfoRepository.savedUserIds shouldBe listOf(userBId) + } + } + + describe("setProfile") { + it("기념일이 이미 있으면 PROFILE_SETUP 상태 사용자는 프로필 설정 시 완료된다") { + val coupleInfo = createCoupleInfo(anniversaryDate = anniversaryDate) + val userBOnboarding = createOnboardingInfo(userBId, OnboardingStatus.PROFILE_SETUP) + val onboardingInfoRepository = FakeOnboardingInfoRepository(userBOnboarding) + val service = createService(onboardingInfoRepository) + every { userAdditionInfoRepository.findByUserId(userBId) } returns null + every { coupleInfoRepository.findByUserId(userBId) } returns coupleInfo + + service.setProfile(userBId, "keeper") + + userBOnboarding.status shouldBe OnboardingStatus.COMPLETED + onboardingInfoRepository.savedUserIds shouldBe listOf(userBId) + } + } +}) + +private class FakeOnboardingInfoRepository( + vararg onboardingInfos: UserOnboardingInfo, +) : OnboardingInfoRepository { + private val onboardingInfos = onboardingInfos.associateBy { it.userId }.toMutableMap() + + val savedUserIds = mutableListOf() + + override fun findByUserId(userId: Long): UserOnboardingInfo? = onboardingInfos[userId] + + override fun save(onboardingInfo: UserOnboardingInfo): UserOnboardingInfo { + onboardingInfos[onboardingInfo.userId] = onboardingInfo + savedUserIds += onboardingInfo.userId + return onboardingInfo + } + + override fun deleteByUserId(userId: Long) { + onboardingInfos.remove(userId) + } +} + +private fun createCoupleInfo(anniversaryDate: LocalDate? = null) = + CoupleInfo( + id = 1L, + user1Id = 1L, + user2Id = 2L, + inviteCodeId = 10L, + anniversaryDate = anniversaryDate, + ) + +private fun createOnboardingInfo( + userId: Long, + status: OnboardingStatus, +) = UserOnboardingInfo( + userId = userId, + status = status, +) diff --git a/love/domain/src/main/kotlin/com/yapp/love/domain/onboarding/model/Onboarding.kt b/love/domain/src/main/kotlin/com/yapp/love/domain/onboarding/model/Onboarding.kt index a9eefab..7fc2e0b 100644 --- a/love/domain/src/main/kotlin/com/yapp/love/domain/onboarding/model/Onboarding.kt +++ b/love/domain/src/main/kotlin/com/yapp/love/domain/onboarding/model/Onboarding.kt @@ -55,19 +55,31 @@ class UserOnboardingInfo( if (expectedStatus != null && status != expectedStatus) return if (coupleInfo.anniversaryDate != null && status == OnboardingStatus.PROFILE_SETUP) { - status = OnboardingStatus.COMPLETED - completedAt = LocalDateTime.now() + complete() return } status = status.next() if (status == OnboardingStatus.COMPLETED) { - completedAt = LocalDateTime.now() + complete() } } + fun completeAnniversarySetupIfReady(coupleInfo: CoupleInfo): Boolean { + if (coupleInfo.anniversaryDate == null) return false + if (status != OnboardingStatus.ANNIVERSARY_SETUP) return false + + complete() + return true + } + + private fun complete() { + status = OnboardingStatus.COMPLETED + completedAt = LocalDateTime.now() + } + companion object { fun create(userId: Long) = UserOnboardingInfo(userId = userId) }