Skip to content

Commit 22787ff

Browse files
committed
Merge branch 'develop' of https://github.com/Runnect/Runnect-Android into feature/feat-my-draw-course-share-edit
2 parents 75d7956 + 5661733 commit 22787ff

File tree

27 files changed

+687
-266
lines changed

27 files changed

+687
-266
lines changed

app/src/debug/java/com/runnect/runnect/developer/RunnectDeveloperActivity.kt

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import com.runnect.runnect.R
1717
import com.runnect.runnect.application.ApiMode
1818
import com.runnect.runnect.application.ApplicationClass
1919
import com.runnect.runnect.application.PreferenceManager
20-
import com.runnect.runnect.data.service.TokenAuthenticator
21-
import com.runnect.runnect.presentation.mypage.setting.accountinfo.MySettingAccountInfoFragment
2220
import com.runnect.runnect.util.custom.toast.RunnectToast
21+
import com.runnect.runnect.util.preference.AuthUtil.getAccessToken
22+
import com.runnect.runnect.util.preference.AuthUtil.getNewToken
23+
import com.runnect.runnect.util.preference.AuthUtil.saveToken
24+
import com.runnect.runnect.util.preference.StatusType.LoginStatus
2325
import kotlinx.coroutines.Dispatchers
2426
import kotlinx.coroutines.delay
2527
import kotlinx.coroutines.launch
@@ -44,15 +46,15 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev
4446

4547
private fun initUserInfo() {
4648
val ctx: Context = context ?: return
47-
val accessToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_ACCESS) ?: ""
48-
val refreshToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_REFRESH) ?: ""
49+
val accessToken = ctx.getAccessToken()
50+
val refreshToken = ctx.getNewToken()
4951

5052
setPreferenceSummary("dev_pref_key_access_token", accessToken)
5153
setPreferenceSummary("dev_pref_key_refresh_token", refreshToken)
5254
}
5355

5456
private fun initApiMode() {
55-
val ctx:Context = context ?: ApplicationClass.appContext
57+
val ctx: Context = context ?: ApplicationClass.appContext
5658
val currentApi = ApiMode.getCurrentApiMode(ctx)
5759

5860
findPreference<ListPreference>("dev_pref_key_api_mode")?.apply {
@@ -70,10 +72,11 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev
7072

7173
PreferenceManager.apply {
7274
setString(ctx, ApplicationClass.API_MODE, selectItem)
73-
setString(ctx, MySettingAccountInfoFragment.TOKEN_KEY_ACCESS, "none")
74-
setString(ctx, MySettingAccountInfoFragment.TOKEN_KEY_REFRESH, "none")
7575
}
76-
76+
ctx.saveToken(
77+
accessToken = LoginStatus.NONE.value,
78+
refreshToken = LoginStatus.NONE.value
79+
)
7780
destroyApp(ctx)
7881
true
7982
}
@@ -93,9 +96,15 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev
9396
val naviBarHeight = getNaviBarHeight(windowManager)
9497

9598
with(metrics) {
96-
setPreferenceSummary("dev_pref_display_ratio", "$widthPixels x ${heightPixels + statusBarHeight + naviBarHeight}")
99+
setPreferenceSummary(
100+
"dev_pref_display_ratio",
101+
"$widthPixels x ${heightPixels + statusBarHeight + naviBarHeight}"
102+
)
97103
setPreferenceSummary("dev_pref_display_density", "${densityDpi}dp")
98-
setPreferenceSummary("dev_pref_display_resource_bucket", getDeviceResourseBucket(this))
104+
setPreferenceSummary(
105+
"dev_pref_display_resource_bucket",
106+
getDeviceResourseBucket(this)
107+
)
99108
}
100109
}
101110

@@ -116,7 +125,8 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev
116125
private fun getStatusBarHeight(windowManager: WindowManager): Int {
117126
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
118127
val windowMetrics = windowManager.currentWindowMetrics
119-
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars())
128+
val insets =
129+
windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars())
120130
insets.top
121131
} else {
122132
0
@@ -126,7 +136,8 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev
126136
private fun getNaviBarHeight(windowManager: WindowManager): Int {
127137
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
128138
val windowMetrics = windowManager.currentWindowMetrics
129-
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars())
139+
val insets =
140+
windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars())
130141
insets.bottom
131142
} else {
132143
0
@@ -157,7 +168,8 @@ class RunnectDeveloperActivity : AppCompatActivity(R.layout.activity_runnect_dev
157168

158169
private fun destroyApp(context: Context) {
159170
lifecycleScope.launch(Dispatchers.Main) {
160-
RunnectToast.createToast(context, getString(R.string.dev_mode_require_restart)).show()
171+
RunnectToast.createToast(context, getString(R.string.dev_mode_require_restart))
172+
.show()
161173
delay(3000)
162174

163175
activity?.finishAffinity() //루트액티비티 종료
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.runnect.runnect.data.dto.response.base
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class ErrorResponse<T>(
8+
@SerialName("status")
9+
val status: Int,
10+
@SerialName("success")
11+
val success: Boolean?,
12+
@SerialName("message")
13+
val message: String?,
14+
@SerialName("error")
15+
val error: String?,
16+
@SerialName("data")
17+
val data: T? = null
18+
)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.runnect.runnect.data.network.calladapter
2+
3+
import com.google.gson.Gson
4+
import com.runnect.runnect.data.dto.response.base.ErrorResponse
5+
import com.runnect.runnect.domain.common.RunnectException
6+
import retrofit2.Call
7+
import retrofit2.Callback
8+
import retrofit2.Response
9+
import okhttp3.Request
10+
import okio.Timeout
11+
12+
class ResultCall<T>(private val call: Call<T>) : Call<Result<T>> {
13+
14+
private val gson = Gson()
15+
16+
override fun execute(): Response<Result<T>> {
17+
throw UnsupportedOperationException("ResultCall doesn't support execute")
18+
}
19+
20+
override fun enqueue(callback: Callback<Result<T>>) {
21+
call.enqueue(object : Callback<T> {
22+
override fun onResponse(call: Call<T>, response: Response<T>) {
23+
val apiResult = if (response.isSuccessful) {
24+
response.body()?.let {
25+
Result.success(it)
26+
} ?: Result.failure(
27+
RunnectException(
28+
code = response.code(),
29+
message = ERROR_MSG_RESPONSE_IS_NULL
30+
)
31+
)
32+
} else {
33+
Result.failure(parseErrorResponse(response))
34+
}
35+
36+
callback.onResponse(
37+
this@ResultCall,
38+
Response.success(apiResult)
39+
)
40+
}
41+
42+
override fun onFailure(call: Call<T>, t: Throwable) {
43+
callback.onFailure(this@ResultCall, t)
44+
}
45+
})
46+
}
47+
48+
private fun parseErrorResponse(response: Response<*>): RunnectException {
49+
val errorJson = response.errorBody()?.string()
50+
51+
return runCatching {
52+
val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java)
53+
val message = errorBody?.run {
54+
message ?: error ?: ERROR_MSG_COMMON
55+
}
56+
57+
RunnectException(
58+
code = errorBody.status,
59+
message = message
60+
)
61+
}.getOrElse {
62+
RunnectException(
63+
code = response.code(),
64+
message = ERROR_MSG_COMMON
65+
)
66+
}
67+
}
68+
69+
override fun clone(): Call<Result<T>> = ResultCall(call.clone())
70+
override fun isExecuted(): Boolean = call.isExecuted
71+
override fun cancel() = call.cancel()
72+
override fun isCanceled(): Boolean = call.isCanceled
73+
override fun request(): Request = call.request()
74+
override fun timeout(): Timeout = call.timeout()
75+
76+
companion object {
77+
private const val ERROR_MSG_COMMON = "알 수 없는 에러가 발생하였습니다."
78+
private const val ERROR_MSG_RESPONSE_IS_NULL = "데이터를 불러올 수 없습니다."
79+
}
80+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.runnect.runnect.data.network.calladapter
2+
3+
import retrofit2.Call
4+
import retrofit2.CallAdapter
5+
import java.lang.reflect.Type
6+
7+
class ResultCallAdapter<T>(
8+
private val responseType: Type
9+
) : CallAdapter<T, Call<Result<T>>> {
10+
11+
override fun responseType() = responseType
12+
13+
// Retrofit의 Call을 Result<>로 변환
14+
override fun adapt(call: Call<T>): Call<Result<T>> {
15+
return ResultCall(call)
16+
}
17+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.runnect.runnect.data.network.calladapter
2+
3+
import retrofit2.Call
4+
import retrofit2.CallAdapter
5+
import retrofit2.Retrofit
6+
import java.lang.reflect.ParameterizedType
7+
import java.lang.reflect.Type
8+
9+
class ResultCallAdapterFactory private constructor() : CallAdapter.Factory() {
10+
11+
override fun get(
12+
returnType: Type,
13+
annotations: Array<Annotation>,
14+
retrofit: Retrofit
15+
): CallAdapter<*, *>? {
16+
// 최상위 타입이 Call인지 체크(suspend로 선언시 Call로 감싸짐)
17+
if (getRawType(returnType) != Call::class.java) {
18+
return null
19+
}
20+
21+
check(returnType is ParameterizedType) {
22+
"Call return type must be parameterized as Call<Foo> or Call<out Foo>"
23+
}
24+
25+
val responseType = getParameterUpperBound(0, returnType)
26+
if (getRawType(responseType) != Result::class.java) {
27+
return null
28+
}
29+
30+
check(responseType is ParameterizedType) {
31+
"ApiResult return type must be parameterized as ApiResult<Foo> or ApiResult<out Foo>"
32+
}
33+
34+
return ResultCallAdapter<Any>(
35+
getParameterUpperBound(
36+
0,
37+
responseType
38+
)
39+
)
40+
}
41+
42+
companion object {
43+
@JvmStatic
44+
fun create() = ResultCallAdapterFactory()
45+
}
46+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.runnect.runnect.data.network.interceptor
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.JsonParseException
5+
import com.google.gson.JsonSyntaxException
6+
import com.runnect.runnect.data.dto.response.base.BaseResponse
7+
import okhttp3.Interceptor
8+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
9+
import okhttp3.Response
10+
import okhttp3.ResponseBody.Companion.toResponseBody
11+
import timber.log.Timber
12+
13+
/**
14+
* BaseResponse에서 data만 추출 (불필요한 래핑 제거)
15+
* - 서버에서 내려준 형식이 아니라면 응답 그대로 반환
16+
*/
17+
class ResponseInterceptor : Interceptor {
18+
19+
private val gson = Gson()
20+
21+
override fun intercept(chain: Interceptor.Chain): Response {
22+
val originalResponse = chain.proceed(chain.request())
23+
if (!originalResponse.isSuccessful) return originalResponse
24+
25+
val bodyString = originalResponse.peekBody(Long.MAX_VALUE).string()
26+
val newResponseBodyString = jsonToBaseResponse(bodyString)?.let {
27+
it.toResponseBody("application/json".toMediaTypeOrNull())
28+
} ?: return originalResponse
29+
30+
return originalResponse.newBuilder()
31+
.code(originalResponse.code)
32+
.body(newResponseBodyString)
33+
.build()
34+
.apply {
35+
Timber.v("""\n
36+
origin = ${originalResponse.peekBody(Long.MAX_VALUE).string()}
37+
new = ${this.peekBody(Long.MAX_VALUE).string()}
38+
""".trimIndent()
39+
)
40+
}
41+
}
42+
43+
private fun jsonToBaseResponse(body: String): String? {
44+
return try {
45+
val baseResponse = gson.fromJson(body, BaseResponse::class.java)
46+
gson.toJson(baseResponse.data)
47+
} catch (e: JsonSyntaxException) {
48+
null // JSON 구문 분석 오류 발생 시 원래 형식을 반환
49+
} catch (e: JsonParseException) {
50+
null // JSON 파싱 오류 발생 시 원래 형식을 반환
51+
} catch (e: Exception) {
52+
null // 기타 예외 발생 시 원래 형식을 반환
53+
}
54+
}
55+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.runnect.runnect.data.network
2+
3+
import com.runnect.runnect.domain.common.RunnectException
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.flow
6+
7+
fun <R, D> Result<R>.mapToFlowResult(
8+
mapper: (R) -> D
9+
): Flow<Result<D>> = flow {
10+
val result = when {
11+
this@mapToFlowResult.isSuccess -> Result.success(
12+
// CallAdapter에서 body가 null인 경우도 걸러주고 있으므로
13+
// Result.success의 데이터가 null인 경우는 없을듯함
14+
mapper(this@mapToFlowResult.getOrNull()!!)
15+
)
16+
17+
else -> Result.failure(
18+
this@mapToFlowResult.exceptionOrNull() ?: RunnectException()
19+
)
20+
}
21+
22+
emit(result)
23+
}

app/src/main/java/com/runnect/runnect/data/repository/CourseRepositoryImpl.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,27 @@ import com.runnect.runnect.data.dto.response.ResponsePostMyHistory
1313
import com.runnect.runnect.data.dto.response.ResponsePutMyDrawCourse
1414
import com.runnect.runnect.data.dto.response.ResponsePostDiscoverUpload
1515
import com.runnect.runnect.data.dto.response.ResponsePostScrap
16+
import com.runnect.runnect.data.network.mapToFlowResult
1617
import com.runnect.runnect.data.source.remote.RemoteCourseDataSource
1718
import com.runnect.runnect.domain.entity.DiscoverSearchCourse
1819
import com.runnect.runnect.domain.entity.DiscoverMultiViewItem.*
1920
import com.runnect.runnect.domain.entity.DiscoverUploadCourse
2021
import com.runnect.runnect.domain.entity.EditableCourseDetail
2122
import com.runnect.runnect.domain.entity.RecommendCoursePagingData
2223
import com.runnect.runnect.domain.repository.CourseRepository
24+
import kotlinx.coroutines.flow.Flow
2325
import okhttp3.MultipartBody
2426
import okhttp3.RequestBody
2527
import retrofit2.Response
2628
import javax.inject.Inject
2729

2830
class CourseRepositoryImpl @Inject constructor(private val remoteCourseDataSource: RemoteCourseDataSource) :
2931
CourseRepository {
30-
override suspend fun getMarathonCourse(): Result<List<MarathonCourse>?> = runCatching {
31-
remoteCourseDataSource.getMarathonCourse().data?.toMarathonCourses()
32+
33+
override suspend fun getMarathonCourse(): Flow<Result<List<MarathonCourse>>> {
34+
return remoteCourseDataSource.getMarathonCourse().mapToFlowResult {
35+
it.toMarathonCourses()
36+
}
3237
}
3338

3439
override suspend fun getRecommendCourse(

0 commit comments

Comments
 (0)