diff --git a/app/src/main/java/com/yapp/twix/di/InitKoin.kt b/app/src/main/java/com/yapp/twix/di/InitKoin.kt index 488e606d..87c1af4d 100644 --- a/app/src/main/java/com/yapp/twix/di/InitKoin.kt +++ b/app/src/main/java/com/yapp/twix/di/InitKoin.kt @@ -1,6 +1,7 @@ package com.yapp.twix.di import android.content.Context +import com.twix.data.di.dataModule import com.twix.network.di.networkModule import com.twix.ui.di.uiModule import org.koin.android.ext.koin.androidContext @@ -19,6 +20,7 @@ fun initKoin( addAll(extraModules) addAll(featureModules) addAll(networkModule) + addAll(dataModule) add(uiModule) }, ) diff --git a/core/design-system/src/main/java/com/twix/designsystem/.gitkeep b/core/design-system/src/main/java/com/twix/designsystem/.gitkeep deleted file mode 100644 index 379ad9b7..00000000 --- a/core/design-system/src/main/java/com/twix/designsystem/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# This file ensures the directory is tracked by git diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 45bdf02d..6d2e7083 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -42,4 +42,5 @@ android { dependencies { implementation(libs.bundles.ktor) implementation(libs.ktorfit.lib) + ksp(libs.ktorfit.ksp) } diff --git a/core/network/src/main/java/com/twix/network/HttpClientProvider.kt b/core/network/src/main/java/com/twix/network/HttpClientProvider.kt index 4f598072..c468b311 100644 --- a/core/network/src/main/java/com/twix/network/HttpClientProvider.kt +++ b/core/network/src/main/java/com/twix/network/HttpClientProvider.kt @@ -5,7 +5,9 @@ import io.ktor.client.HttpClientConfig import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.ANDROID import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.http.ContentType import io.ktor.http.contentType @@ -24,6 +26,18 @@ internal object HttpClientProvider { configureLogging(isDebug) configureTimeout() configureDefaultRequest(baseUrl) + + // TODO : 토큰 관련 기능 구현 후 적용 +// install(Auth) { +// bearer { +// loadTokens { +// BearerTokens( +// accessToken = "", +// refreshToken = "", +// ) +// } +// } +// } } private fun HttpClientConfig<*>.configureContentNegotiation(isDebug: Boolean) { @@ -46,6 +60,7 @@ internal object HttpClientProvider { } else { LogLevel.NONE } + logger = Logger.ANDROID sanitizeHeader { header -> header == SANITIZE_HEADER } } diff --git a/core/network/src/main/java/com/twix/network/di/ApiServiceModule.kt b/core/network/src/main/java/com/twix/network/di/ApiServiceModule.kt index bfe215b8..7116c8c0 100644 --- a/core/network/src/main/java/com/twix/network/di/ApiServiceModule.kt +++ b/core/network/src/main/java/com/twix/network/di/ApiServiceModule.kt @@ -1,7 +1,13 @@ package com.twix.network.di +import com.twix.network.service.OnboardingService +import com.twix.network.service.createOnboardingService +import de.jensklingenberg.ktorfit.Ktorfit import org.koin.dsl.module internal val apiServiceModule = module { + single { + get().createOnboardingService() + } } diff --git a/core/network/src/main/java/com/twix/network/di/HttpClientModule.kt b/core/network/src/main/java/com/twix/network/di/HttpClientModule.kt index 565d2c86..dfa3b254 100644 --- a/core/network/src/main/java/com/twix/network/di/HttpClientModule.kt +++ b/core/network/src/main/java/com/twix/network/di/HttpClientModule.kt @@ -16,7 +16,7 @@ internal val httpClientModule = ) } - single { + single { Ktorfit .Builder() .baseUrl(BuildConfig.BASE_URL) diff --git a/core/network/src/main/java/com/twix/network/di/NetworkModule.kt b/core/network/src/main/java/com/twix/network/di/NetworkModule.kt index 60c94728..8ebfe092 100644 --- a/core/network/src/main/java/com/twix/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/twix/network/di/NetworkModule.kt @@ -3,4 +3,5 @@ package com.twix.network.di val networkModule = listOf( httpClientModule, + apiServiceModule, ) diff --git a/core/network/src/main/java/com/twix/network/model/request/AnniversaryRequest.kt b/core/network/src/main/java/com/twix/network/model/request/AnniversaryRequest.kt new file mode 100644 index 00000000..4506fe76 --- /dev/null +++ b/core/network/src/main/java/com/twix/network/model/request/AnniversaryRequest.kt @@ -0,0 +1,8 @@ +package com.twix.network.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class AnniversaryRequest( + val anniversaryDate: String, +) diff --git a/core/network/src/main/java/com/twix/network/model/request/CoupleConnectionRequest.kt b/core/network/src/main/java/com/twix/network/model/request/CoupleConnectionRequest.kt new file mode 100644 index 00000000..4c5461fe --- /dev/null +++ b/core/network/src/main/java/com/twix/network/model/request/CoupleConnectionRequest.kt @@ -0,0 +1,8 @@ +package com.twix.network.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class CoupleConnectionRequest( + val inviteCode: String, +) diff --git a/core/network/src/main/java/com/twix/network/model/request/ProfileRequest.kt b/core/network/src/main/java/com/twix/network/model/request/ProfileRequest.kt new file mode 100644 index 00000000..86501941 --- /dev/null +++ b/core/network/src/main/java/com/twix/network/model/request/ProfileRequest.kt @@ -0,0 +1,8 @@ +package com.twix.network.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class ProfileRequest( + val nickname: String, +) diff --git a/core/network/src/main/java/com/twix/network/model/response/onboarding/InviteCodeResponse.kt b/core/network/src/main/java/com/twix/network/model/response/onboarding/InviteCodeResponse.kt new file mode 100644 index 00000000..56fb14e3 --- /dev/null +++ b/core/network/src/main/java/com/twix/network/model/response/onboarding/InviteCodeResponse.kt @@ -0,0 +1,8 @@ +package com.twix.network.model.response.onboarding + +import kotlinx.serialization.Serializable + +@Serializable +data class InviteCodeResponse( + val inviteCode: String, +) diff --git a/core/network/src/main/java/com/twix/network/model/response/onboarding/OnBoardingStatusResponse.kt b/core/network/src/main/java/com/twix/network/model/response/onboarding/OnBoardingStatusResponse.kt new file mode 100644 index 00000000..4037b796 --- /dev/null +++ b/core/network/src/main/java/com/twix/network/model/response/onboarding/OnBoardingStatusResponse.kt @@ -0,0 +1,8 @@ +package com.twix.network.model.response.onboarding + +import kotlinx.serialization.Serializable + +@Serializable +data class OnBoardingStatusResponse( + val status: String, +) diff --git a/core/network/src/main/java/com/twix/network/service/OnboardingService.kt b/core/network/src/main/java/com/twix/network/service/OnboardingService.kt new file mode 100644 index 00000000..6cd55e5d --- /dev/null +++ b/core/network/src/main/java/com/twix/network/service/OnboardingService.kt @@ -0,0 +1,33 @@ +package com.twix.network.service + +import com.twix.network.model.request.AnniversaryRequest +import com.twix.network.model.request.CoupleConnectionRequest +import com.twix.network.model.request.ProfileRequest +import com.twix.network.model.response.onboarding.InviteCodeResponse +import com.twix.network.model.response.onboarding.OnBoardingStatusResponse +import de.jensklingenberg.ktorfit.http.Body +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.POST + +interface OnboardingService { + @POST("onboarding/anniversary") + suspend fun anniversarySetup( + @Body request: AnniversaryRequest, + ) + + @POST("onboarding/couple-connection") + suspend fun coupleConnection( + @Body request: CoupleConnectionRequest, + ) + + @POST("onboarding/profile") + suspend fun profileSetup( + @Body request: ProfileRequest, + ) + + @GET("onboarding/invite-code") + suspend fun fetchInviteCode(): InviteCodeResponse + + @GET("onboarding/status") + suspend fun fetchOnBoardingStatus(): OnBoardingStatusResponse +} diff --git a/data/src/main/java/com/twix/data/.gitkeep b/data/src/main/java/com/twix/data/.gitkeep deleted file mode 100644 index 379ad9b7..00000000 --- a/data/src/main/java/com/twix/data/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# This file ensures the directory is tracked by git diff --git a/data/src/main/java/com/twix/data/di/DataModule.kt b/data/src/main/java/com/twix/data/di/DataModule.kt new file mode 100644 index 00000000..b4bb1999 --- /dev/null +++ b/data/src/main/java/com/twix/data/di/DataModule.kt @@ -0,0 +1,6 @@ +package com.twix.data.di + +val dataModule = + listOf( + repositoryModule, + ) diff --git a/data/src/main/java/com/twix/data/di/RepositoryModule.kt b/data/src/main/java/com/twix/data/di/RepositoryModule.kt new file mode 100644 index 00000000..bc50e95d --- /dev/null +++ b/data/src/main/java/com/twix/data/di/RepositoryModule.kt @@ -0,0 +1,12 @@ +package com.twix.data.di + +import com.twix.data.repository.DefaultOnboardingRepository +import com.twix.domain.repository.OnBoardingRepository +import org.koin.dsl.module + +internal val repositoryModule = + module { + single { + DefaultOnboardingRepository(get()) + } + } diff --git a/data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt b/data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt new file mode 100644 index 00000000..a9f998f6 --- /dev/null +++ b/data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt @@ -0,0 +1,35 @@ +package com.twix.data.repository + +import com.twix.domain.model.InviteCode +import com.twix.domain.model.OnboardingStatus +import com.twix.domain.repository.OnBoardingRepository +import com.twix.network.model.request.AnniversaryRequest +import com.twix.network.model.request.CoupleConnectionRequest +import com.twix.network.model.request.ProfileRequest +import com.twix.network.service.OnboardingService + +class DefaultOnboardingRepository( + private val service: OnboardingService, +) : OnBoardingRepository { + override suspend fun fetchInviteCode(): InviteCode { + val response = service.fetchInviteCode() + return InviteCode(response.inviteCode) + } + + override suspend fun fetchOnboardingStatus(): OnboardingStatus { + val response = service.fetchOnBoardingStatus() + return OnboardingStatus.from(response.status) + } + + override suspend fun anniversarySetup(request: String) { + service.anniversarySetup(AnniversaryRequest(request)) + } + + override suspend fun coupleConnection(request: String) { + service.coupleConnection(CoupleConnectionRequest(request)) + } + + override suspend fun profileSetup(request: String) { + service.profileSetup(ProfileRequest(request)) + } +} diff --git a/domain/src/main/java/com/twix/domain/.gitkeep b/domain/src/main/java/com/twix/domain/.gitkeep deleted file mode 100644 index 379ad9b7..00000000 --- a/domain/src/main/java/com/twix/domain/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# This file ensures the directory is tracked by git diff --git a/domain/src/main/java/com/twix/domain/model/InviteCode.kt b/domain/src/main/java/com/twix/domain/model/InviteCode.kt new file mode 100644 index 00000000..0060a090 --- /dev/null +++ b/domain/src/main/java/com/twix/domain/model/InviteCode.kt @@ -0,0 +1,18 @@ +package com.twix.domain.model + +@JvmInline +value class InviteCode( + val value: String, +) { + init { + require(value.length == INVITE_CODE_LENGTH) { INVALID_INVITE_CODE_EXCEPTION } + require(INVITE_CODE_REGEX.matches(value)) { INVALID_INVITE_CODE_EXCEPTION } + } + + companion object { + private val INVITE_CODE_REGEX = Regex("^[A-Z0-9]+$") + private const val INVITE_CODE_LENGTH = 8 + private const val INVALID_INVITE_CODE_EXCEPTION = + "InviteCode must be 8 characters of uppercase letters and digits" + } +} diff --git a/domain/src/main/java/com/twix/domain/model/OnboardingStatus.kt b/domain/src/main/java/com/twix/domain/model/OnboardingStatus.kt new file mode 100644 index 00000000..ab5272c8 --- /dev/null +++ b/domain/src/main/java/com/twix/domain/model/OnboardingStatus.kt @@ -0,0 +1,20 @@ +package com.twix.domain.model + +enum class OnboardingStatus { + COUPLE_CONNECTION, + PROFILE_SETUP, + ANNIVERSARY_SETUP, + COMPLETED, + ; + + companion object { + fun from(status: String): OnboardingStatus = + runCatching { + valueOf(status.trim().uppercase()) + }.getOrElse { + throw IllegalArgumentException(UNKNOWN_STATUS.format(status)) + } + + private const val UNKNOWN_STATUS = "UNKNOWN_STATUS: %s" + } +} diff --git a/domain/src/main/java/com/twix/domain/repository/OnBoardingRepository.kt b/domain/src/main/java/com/twix/domain/repository/OnBoardingRepository.kt new file mode 100644 index 00000000..76173f04 --- /dev/null +++ b/domain/src/main/java/com/twix/domain/repository/OnBoardingRepository.kt @@ -0,0 +1,16 @@ +package com.twix.domain.repository + +import com.twix.domain.model.InviteCode +import com.twix.domain.model.OnboardingStatus + +interface OnBoardingRepository { + suspend fun anniversarySetup(request: String) + + suspend fun coupleConnection(request: String) + + suspend fun profileSetup(request: String) + + suspend fun fetchInviteCode(): InviteCode + + suspend fun fetchOnboardingStatus(): OnboardingStatus +} diff --git a/domain/src/test/java/com/twix/domain/model/InviteCodeTest.kt b/domain/src/test/java/com/twix/domain/model/InviteCodeTest.kt new file mode 100644 index 00000000..ff5435e6 --- /dev/null +++ b/domain/src/test/java/com/twix/domain/model/InviteCodeTest.kt @@ -0,0 +1,43 @@ +package com.twix.domain.model + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class InviteCodeTest { + @Test + fun `유효한 초대 코드는 정상적으로 생성된다`() { + val inviteCode = InviteCode("AB12CD34") + + assertThat(inviteCode.value).isEqualTo("AB12CD34") + } + + @ParameterizedTest + @ValueSource( + strings = [ + "ab12CD34", + "AB12CD3!", + " ", + ], + ) + fun `규칙에 맞지 않는 초대 코드는 예외가 발생한다`(invalidCode: String) { + assertThatThrownBy { InviteCode(invalidCode) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("InviteCode must be 8 characters of uppercase letters and digits") + } + + @ParameterizedTest + @ValueSource( + strings = [ + "ABC123", + "AB12CD345", + ], + ) + fun `8자리가 아닌 초대 코드는 예외가 발생한다`(invalidLengthCode: String) { + assertThatThrownBy { InviteCode(invalidLengthCode) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("InviteCode must be 8 characters of uppercase letters and digits") + } +} diff --git a/domain/src/test/java/com/twix/domain/model/OnboardingStatusTest.kt b/domain/src/test/java/com/twix/domain/model/OnboardingStatusTest.kt new file mode 100644 index 00000000..3e7a6b0d --- /dev/null +++ b/domain/src/test/java/com/twix/domain/model/OnboardingStatusTest.kt @@ -0,0 +1,41 @@ +package com.twix.domain.model + +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import kotlin.test.assertEquals + +class OnboardingStatusTest { + @ParameterizedTest + @CsvSource( + "COUPLE_CONNECTION,COUPLE_CONNECTION", + "PROFILE_SETUP,PROFILE_SETUP", + "ANNIVERSARY_SETUP,ANNIVERSARY_SETUP", + "COMPLETED,COMPLETED", + ) + @DisplayName("올바른 OnboardingStatus enum을 반환한다") + fun `유효한 상태 문자열이 주어지면 올바른 OnboardingStatus enum을 반환한다`( + input: String, + expected: OnboardingStatus, + ) { + // when + val result = OnboardingStatus.from(input) + + // then + assertEquals(result, expected) + } + + @Test + fun `유효하지 않은 상태 문자열이 주어지면 IllegalArgumentException을 던진다`() { + // given + val invalidStatus = "UNKNOWN" + + // when + assertThatThrownBy { OnboardingStatus.from(invalidStatus) } + // then + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("UNKNOWN_STATUS: $invalidStatus") + } +}