From 62bb831830de71ce3eb89014396f29695435b237 Mon Sep 17 00:00:00 2001 From: JUNWON LEE <87055456+murjune@users.noreply.github.com> Date: Thu, 1 Feb 2024 05:34:11 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20network=20=EA=B8=B0=EC=B4=88=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85,=20RetrofitModule,=20ServiceModule=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [build] kotlin-Serialization id 수정 * [init] network 모듈 * [build] hilt : migrate from kapt to ksp * [build] kotlin config * [build] testing 모듈에 JVM 플러그인 적용 * [build] ksp, hilt verson up * [build] Build-logic refactoring * [init] dataStore Module * [feat] UserCodeDataStore * [feat] DataStoreModule * [refactor] Preference 만드는 로직 분리 * [build] ktlint 적용 * [build] set ktlint, BASE_URL * [fix] deprecated된 manifast package 삭제 (nameSpace로 대체됨) * [build] Test 의존성 추가 * [build] META-INF/LICENSE* 파일 무시 * [add] allow usesCleartextTraffic, INTERNET Permission * [build] core:network BuildConfig, test, kotlinx-serialization 세팅 * [feat] RetrofitModule * [feat] ServiceModule, MatchingService * [feat] MatchingResponse * [feat] BaseResponse * [chore] postMatch -> matchProfile네이밍 변경 * [test] matchProfile * [chore] 불필요한 파일 삭제 * [sample] 서버 통신 Test~ (PR 머지 전에 삭제할 것임) * [CI] github secret key -> local property에 주입 * [chore] ktlint * [refactor] 줄 바꿈, ignoreUnKnownKeys 옵션 삭제 * [chore] naming 변경 * [refactor] release 버전도 FUNCH_DEBUG_BASE_URL 추가 * [refactor] Response 분리 --- .github/workflows/PR_Builder.yml | 16 +-- app/build.gradle.kts | 6 +- app/src/main/AndroidManifest.xml | 5 +- .../main/java/com/moya/funch/MainActivity.kt | 26 +++++ .../funch/plugins/AndroidLibraryPlugin.kt | 4 - .../com/moya/funch/plugins/JUnit5Plugin.kt | 19 ++++ core/network/build.gradle.kts | 32 ++++++ .../java/com/moya/funch/network/MyClass.kt | 4 - .../moya/funch/network/di/RetrofitModule.kt | 67 +++++++++++++ .../moya/funch/network/di/ServiceModule.kt | 18 ++++ .../network/dto/request/MatchingRequest.kt | 10 ++ .../network/dto/response/BaseResponse.kt | 14 +++ .../dto/response/match/ChemistryResponse.kt | 12 +++ .../dto/response/match/MatchingResponse.kt | 19 ++++ .../dto/response/match/RecommendResponse.kt | 10 ++ .../dto/response/match/SubwayResponse.kt | 12 +++ .../dto/response/profile/ProfileResponse.kt | 20 ++++ .../funch/network/service/MatchingService.kt | 14 +++ .../network/service/MatchingServiceTest.kt | 99 +++++++++++++++++++ .../network/src/test/res/matching_result.json | 34 +++++++ feature/profile/src/main/AndroidManifest.xml | 2 +- 21 files changed, 423 insertions(+), 20 deletions(-) delete mode 100644 core/network/src/main/java/com/moya/funch/network/MyClass.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/di/RetrofitModule.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/di/ServiceModule.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/dto/request/MatchingRequest.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/dto/response/BaseResponse.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/dto/response/match/ChemistryResponse.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/dto/response/match/MatchingResponse.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/dto/response/match/RecommendResponse.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/dto/response/match/SubwayResponse.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/dto/response/profile/ProfileResponse.kt create mode 100644 core/network/src/main/java/com/moya/funch/network/service/MatchingService.kt create mode 100644 core/network/src/test/java/com/moya/funch/network/service/MatchingServiceTest.kt create mode 100644 core/network/src/test/res/matching_result.json diff --git a/.github/workflows/PR_Builder.yml b/.github/workflows/PR_Builder.yml index 81cc6724..b84c851b 100644 --- a/.github/workflows/PR_Builder.yml +++ b/.github/workflows/PR_Builder.yml @@ -36,12 +36,12 @@ jobs: # echo $GOOGLE_SERVICES >> ./app/google-services.json # cat ./app/google-services.json # -# - name: Create Local Properties -# run: touch local.properties -# -# - name: Access Local Properties -# env: -# HOST_URI: ${{ secrets.HOST_URI }} + - name: Create Local Properties + run: touch local.properties + + - name: Access Local Properties + env: + FUNCH_DEBUG_BASE_URL: ${{ secrets.FUNCH_DEBUG_BASE_URL }} # HOST_RELEASE_URI: ${{ secrets.HOST_RELEASE_URI }} # KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} # KAKAO_REDIRECT_SCHEME: ${{ secrets.KAKAO_REDIRECT_SCHEME }} @@ -50,8 +50,8 @@ jobs: # KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} # KEY_ALIAS: ${{ secrets.KEY_ALIAS }} # STORE_FILE: ${{ secrets.STORE_FILE }} -# run: | -# echo HOST_URI=\"$HOST_URI\" >> local.properties + run: | + echo FUNCH_DEBUG_BASE_URL=\"FUNCH_DEBUG_BASE_URL\" >> local.properties # echo HOST_RELEASE_URI=\"$HOST_RELEASE_URI\" >> local.properties # echo KAKAO_NATIVE_APP_KEY=\"$KAKAO_NATIVE_APP_KEY\" >> local.properties # echo KAKAO_REDIRECT_SCHEME=\"$KAKAO_REDIRECT_SCHEME\" >> local.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 23b720fc..91a0d350 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,10 @@ plugins { android { namespace = "com.moya.funch" + packaging { + resources.excludes.add("META-INF/LICENSE*") + } + defaultConfig { applicationId = "com.moya.funch" versionCode = libs.versions.versionCode.get().toInt() @@ -30,7 +34,7 @@ android { dependencies { // core implementation(projects.core.designsystem) - + implementation(projects.core.network) // @murjune TODO : 삭제 // feature implementation(projects.feature.profile) // implementation(projects.feature.match) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 94dd87a2..9ffb357d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,9 @@ + xmlns:tools="http://schemas.android.com/tools"> + { configureKotlinAndroid(this) defaultConfig.targetSdk = libs.findVersion("targetSdk").get().requiredVersion.toInt() } - - dependencies { - add("testImplementation", kotlin("test")) - } } } diff --git a/build-logic/convention/src/main/kotlin/com/moya/funch/plugins/JUnit5Plugin.kt b/build-logic/convention/src/main/kotlin/com/moya/funch/plugins/JUnit5Plugin.kt index bc39682d..2681015c 100644 --- a/build-logic/convention/src/main/kotlin/com/moya/funch/plugins/JUnit5Plugin.kt +++ b/build-logic/convention/src/main/kotlin/com/moya/funch/plugins/JUnit5Plugin.kt @@ -1,14 +1,33 @@ package com.moya.funch.plugins +import com.android.build.gradle.BaseExtension import com.moya.funch.plugins.utils.libs import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.kotlin +import org.gradle.kotlin.dsl.withType class JUnit5Plugin : Plugin { override fun apply(target: Project): Unit = with(target) { + + extensions.getByType().apply { + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + } + + tasks.withType { + useJUnitPlatform() + } + dependencies { + add("testImplementation", kotlin("test")) add("testImplementation", libs.findBundle("junit5").get()) add("testImplementation", libs.findLibrary("truth").get()) } diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 2825d6b6..c1d4c7c5 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -1,14 +1,46 @@ +import org.jetbrains.kotlin.konan.properties.Properties + plugins { + alias(libs.plugins.ktlint) alias(libs.plugins.funch.android.library) + alias(libs.plugins.funch.kotlinx.serialization) + alias(libs.plugins.funch.junit5) } +val properties = + Properties().apply { + load(rootProject.file("local.properties").inputStream()) + } android { namespace = "com.moja.funch.network" + + buildTypes { + getByName("release") { + buildConfigField( + "String", + "FUNCH_DEBUG_BASE_URL", + properties.getProperty("FUNCH_DEBUG_BASE_URL"), + ) + } + getByName("debug") { + buildConfigField( + "String", + "FUNCH_DEBUG_BASE_URL", + properties.getProperty("FUNCH_DEBUG_BASE_URL"), + ) + } + } } dependencies { implementation(projects.core.datastore) + implementation(projects.core.testing) + implementation(libs.bundles.retrofit) implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp.logging.interceptor) + // test + testImplementation(libs.mockk) + testImplementation(libs.kotlin.coroutines.test) + testImplementation(libs.mockk.webserver) } diff --git a/core/network/src/main/java/com/moya/funch/network/MyClass.kt b/core/network/src/main/java/com/moya/funch/network/MyClass.kt deleted file mode 100644 index 6214831a..00000000 --- a/core/network/src/main/java/com/moya/funch/network/MyClass.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.moya.funch.network - -class MyClass { -} \ No newline at end of file diff --git a/core/network/src/main/java/com/moya/funch/network/di/RetrofitModule.kt b/core/network/src/main/java/com/moya/funch/network/di/RetrofitModule.kt new file mode 100644 index 00000000..11fc3516 --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/di/RetrofitModule.kt @@ -0,0 +1,67 @@ +package com.moya.funch.network.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.moja.funch.network.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitModule { + @Provides + @Singleton + fun provideJson(): Json = + Json { + coerceInputValues = true + } + + @Singleton + @Provides + fun provideJsonConverterFactory(json: Json): Converter.Factory { + return json.asConverterFactory("application/json".toMediaType()) + } + + @Singleton + @Provides + fun provideLoggingInterceptor(): Interceptor = + HttpLoggingInterceptor().setLevel( + if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + }, + ) + + @Singleton + @Provides + fun provideOkHttpClient(logInterceptor: Interceptor): OkHttpClient = + OkHttpClient + .Builder() + .addInterceptor(logInterceptor) + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS).build() + + @Singleton + @Provides + fun provideRetrofit( + client: OkHttpClient, + converterFactory: Converter.Factory, + ): Retrofit = + Retrofit.Builder() + .baseUrl(BuildConfig.FUNCH_DEBUG_BASE_URL) + .client(client) + .addConverterFactory(converterFactory) + .build() +} diff --git a/core/network/src/main/java/com/moya/funch/network/di/ServiceModule.kt b/core/network/src/main/java/com/moya/funch/network/di/ServiceModule.kt new file mode 100644 index 00000000..fdf84879 --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/di/ServiceModule.kt @@ -0,0 +1,18 @@ +package com.moya.funch.network.di + +import com.moya.funch.network.service.MatchingService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.create +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun providesMatchingService(retrofit: Retrofit): MatchingService = retrofit.create() +} diff --git a/core/network/src/main/java/com/moya/funch/network/dto/request/MatchingRequest.kt b/core/network/src/main/java/com/moya/funch/network/dto/request/MatchingRequest.kt new file mode 100644 index 00000000..1b61f0ac --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/dto/request/MatchingRequest.kt @@ -0,0 +1,10 @@ +package com.moya.funch.network.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MatchingRequest( + @SerialName("requestMemberId") val userId: String, + @SerialName("targetMemberCode") val targetCode: String, +) diff --git a/core/network/src/main/java/com/moya/funch/network/dto/response/BaseResponse.kt b/core/network/src/main/java/com/moya/funch/network/dto/response/BaseResponse.kt new file mode 100644 index 00000000..0b3f970d --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/dto/response/BaseResponse.kt @@ -0,0 +1,14 @@ +package com.moya.funch.network.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BaseResponse( + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, + @SerialName("data") + val data: T, +) diff --git a/core/network/src/main/java/com/moya/funch/network/dto/response/match/ChemistryResponse.kt b/core/network/src/main/java/com/moya/funch/network/dto/response/match/ChemistryResponse.kt new file mode 100644 index 00000000..ff854f52 --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/dto/response/match/ChemistryResponse.kt @@ -0,0 +1,12 @@ +package com.moya.funch.network.dto.response.match + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ChemistryResponse( + @SerialName("title") + val title: String = "", + @SerialName("description") + val description: String = "", +) diff --git a/core/network/src/main/java/com/moya/funch/network/dto/response/match/MatchingResponse.kt b/core/network/src/main/java/com/moya/funch/network/dto/response/match/MatchingResponse.kt new file mode 100644 index 00000000..bb1661d7 --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/dto/response/match/MatchingResponse.kt @@ -0,0 +1,19 @@ +package com.moya.funch.network.dto.response.match + +import com.moya.funch.network.dto.response.profile.ProfileResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MatchingResponse( + @SerialName("profile") + val profile: ProfileResponse = ProfileResponse(), + @SerialName("similarity") + val similarity: Int = 0, + @SerialName("chemistryInfos") + val chemistryInfos: List = listOf(), + @SerialName("recommendInfos") + val recommends: List = listOf(), + @SerialName("subwayInfos") + val subways: List = listOf(), +) diff --git a/core/network/src/main/java/com/moya/funch/network/dto/response/match/RecommendResponse.kt b/core/network/src/main/java/com/moya/funch/network/dto/response/match/RecommendResponse.kt new file mode 100644 index 00000000..be3903db --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/dto/response/match/RecommendResponse.kt @@ -0,0 +1,10 @@ +package com.moya.funch.network.dto.response.match + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RecommendResponse( + @SerialName("title") + val title: String = "", +) diff --git a/core/network/src/main/java/com/moya/funch/network/dto/response/match/SubwayResponse.kt b/core/network/src/main/java/com/moya/funch/network/dto/response/match/SubwayResponse.kt new file mode 100644 index 00000000..dc0e3b00 --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/dto/response/match/SubwayResponse.kt @@ -0,0 +1,12 @@ +package com.moya.funch.network.dto.response.match + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SubwayResponse( + @SerialName("lines") + val lines: List = listOf(), + @SerialName("name") + val name: String = "", +) diff --git a/core/network/src/main/java/com/moya/funch/network/dto/response/profile/ProfileResponse.kt b/core/network/src/main/java/com/moya/funch/network/dto/response/profile/ProfileResponse.kt new file mode 100644 index 00000000..9a7621a9 --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/dto/response/profile/ProfileResponse.kt @@ -0,0 +1,20 @@ +package com.moya.funch.network.dto.response.profile + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ProfileResponse( + @SerialName("name") + val name: String = "", + @SerialName("jobGroup") + val jobGroup: String = "", + @SerialName("clubs") + val clubs: List = listOf(), + @SerialName("mbti") + val mbti: String = "", + @SerialName("constellation") + val constellation: String = "", + @SerialName("subwayNames") + val subwayNames: List = listOf(), +) diff --git a/core/network/src/main/java/com/moya/funch/network/service/MatchingService.kt b/core/network/src/main/java/com/moya/funch/network/service/MatchingService.kt new file mode 100644 index 00000000..f9087a20 --- /dev/null +++ b/core/network/src/main/java/com/moya/funch/network/service/MatchingService.kt @@ -0,0 +1,14 @@ +package com.moya.funch.network.service + +import com.moya.funch.network.dto.request.MatchingRequest +import com.moya.funch.network.dto.response.BaseResponse +import com.moya.funch.network.dto.response.match.MatchingResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface MatchingService { + @POST("api/v1/matching") + suspend fun matchProfile( + @Body body: MatchingRequest, + ): BaseResponse +} diff --git a/core/network/src/test/java/com/moya/funch/network/service/MatchingServiceTest.kt b/core/network/src/test/java/com/moya/funch/network/service/MatchingServiceTest.kt new file mode 100644 index 00000000..fa56652b --- /dev/null +++ b/core/network/src/test/java/com/moya/funch/network/service/MatchingServiceTest.kt @@ -0,0 +1,99 @@ +package com.moya.funch.network.service + +import com.google.common.truth.Truth.assertThat +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.moya.funch.network.dto.request.MatchingRequest +import com.moya.funch.network.dto.response.BaseResponse +import com.moya.funch.network.dto.response.match.ChemistryResponse +import com.moya.funch.network.dto.response.match.MatchingResponse +import com.moya.funch.network.dto.response.match.RecommendResponse +import com.moya.funch.network.dto.response.profile.ProfileResponse +import com.moya.funch.rule.CoroutinesTestExtension +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import retrofit2.Retrofit +import retrofit2.create +import java.io.File + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(MockKExtension::class) +@ExtendWith(CoroutinesTestExtension::class) +internal class MatchingServiceTest { + private lateinit var mockWebServer: MockWebServer + private lateinit var matchingService: MatchingService + + @BeforeEach + fun setUp() { + mockWebServer = MockWebServer() + matchingService = + Retrofit.Builder().addConverterFactory( + Json { + ignoreUnknownKeys = true + prettyPrint = true + coerceInputValues = true + }.asConverterFactory("application/json".toMediaType()), + ).baseUrl(mockWebServer.url("")).build().create() + } + + @Test + fun `내 id와 상대방의 code로 Matching 결과를 불러올 수 있다`() = + runTest { + // given + val matchingJson = File("src/test/res/matching_result.json").readText() + val fakeResponse = MockResponse().setBody(matchingJson).setResponseCode(200) + mockWebServer.enqueue(fakeResponse) + val expected = + BaseResponse( + status = 200, + message = "OK", + data = + MatchingResponse( + profile = + ProfileResponse( + "aaa", + "안드로이드", + listOf(), + "ENFJ", + "전갈자리", + listOf(), + ), + similarity = 40, + chemistryInfos = + listOf( + ChemistryResponse( + "기막힌 타이밍에 등장한 너!", + "미정", + ), + ChemistryResponse( + "서로 비슷한 똑! 닮은 꼴", + "미정", + ), + ), + recommends = + listOf( + RecommendResponse("ENFJ"), + RecommendResponse("전갈자리"), + ), + subways = listOf(), + ), + ) + // when + val actualResponse = + matchingService.matchProfile( + MatchingRequest( + userId = "65b6c543ebe5db753688b9dd", + targetCode = "7O2K", + ), + ) + // then + assertThat(actualResponse).isEqualTo(expected) + } +} diff --git a/core/network/src/test/res/matching_result.json b/core/network/src/test/res/matching_result.json new file mode 100644 index 00000000..36590b92 --- /dev/null +++ b/core/network/src/test/res/matching_result.json @@ -0,0 +1,34 @@ +{ + "status": "200", + "message": "OK", + "data": { + "profile": { + "name": "aaa", + "jobGroup": "안드로이드", + "clubs": [], + "mbti": "ENFJ", + "constellation": "전갈자리", + "subwayNames": [] + }, + "similarity": 40, + "chemistryInfos": [ + { + "title": "기막힌 타이밍에 등장한 너!", + "description": "미정" + }, + { + "title": "서로 비슷한 똑! 닮은 꼴", + "description": "미정" + } + ], + "recommendInfos": [ + { + "title": "ENFJ" + }, + { + "title": "전갈자리" + } + ], + "subwayInfos": [] + } +} diff --git a/feature/profile/src/main/AndroidManifest.xml b/feature/profile/src/main/AndroidManifest.xml index a4d98d5d..e1000761 100644 --- a/feature/profile/src/main/AndroidManifest.xml +++ b/feature/profile/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - +