Skip to content

Commit a390166

Browse files
committed
Merge remote-tracking branch 'origin/develop' into feature/refactor-data
# Conflicts: # app/src/main/java/com/runnect/runnect/di/RetrofitModule.kt
2 parents 0af3516 + 17abeac commit a390166

18 files changed

Lines changed: 469 additions & 93 deletions

File tree

README.md

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,75 @@
1-
# <b>🏃🏻 Runnect-Android Repository 🏃🏼</b>
2-
3-
![212252027-210bd23c-e363-4e6f-8e15-1a88bf237bc2 (1)](https://user-images.githubusercontent.com/89737271/216818552-9f9c5e62-b6cb-4128-ab6e-99600493b9a8.png)
4-
5-
<br>
1+
# 🏃🏼‍♀️ Runnect-Android 🏃🏻‍♂️
2+
3+
<img width="800" src="https://github.com/Runnect/Runnect-Android/assets/68090939/d1d3d952-5fd7-4991-9f5b-eed648128930"/>
64

5+
## 💁🏻‍♀️ Introduction
76

7+
Runnect는 Run과 Connect의 합성어로 “능동적인 러닝 문화”와 “러너 간의 네트워킹”으로 러닝에 쉽게 다가가는 세상을 꿈꿉니다.
88

9-
## Contributors 💛
10-
| [@Larry7939](https://github.com/Larry7939) | [@unam98](https://github.com/unam98) |
11-
| :---: | :---: |
12-
|<img width="400" src="https://github.com/Runnect/Runnect-Android/assets/89737271/29aa0044-0653-4573-9957-ddd46090a7fc.jpg">|<img width="400" src="https://github.com/Runnect/Runnect-Android/assets/89737271/58992dce-d7b6-4e33-9d9c-279d5a3a0ef1.jpg">|
13-
|**코스 발견 & 마이페이지**|**코스 그리기 & 보관함**|
9+
코스 그리기와 자유로운 공유를 통해 러너 간의 Connect를 강화하고, 편리한 러닝을 도와줍니다.
1410

15-
<br>
11+
직접 코스를 그려 목표를 설정하고, 다른 러너와 코스를 공유해 보세요! 꾸준한 성취감과 함께 어느새 일상에 자리 잡은 러닝을 발견할 수 있을 거예요!
1612

17-
<h2 id="0.5">
18-
<b>💁 Service introduce</b>
19-
</h2>
13+
## 🎉 Download
2014

21-
### Runnect는 지도에 직접 러닝 코스를 그리고 공유하며 서로를 연결해주고 함께 달릴 수 있는 서비스입니다.
15+
<a href="https://play.google.com/store/apps/details?id=com.runnect.runnect&hl=ko"><img width="40%" src="https://play.google.com/intl/ko/badges/static/images/badges/ko_badge_web_generic.png"/></a>
2216

23-
- 검색을 통해 출발지를 설정하고 지도에 원하는 러닝 코스를 직접 그릴 수 있음
24-
- 생성된 코스는 아카이빙되며 다른 유저들과 공유하거나 나중에 해당 코스를 다시 달려볼 수 있음
25-
- 다른 유저들이 올린 러닝 코스를 스크랩할 수 있으며 본인의 러닝 코스로 설정하여 따라 달려볼 수 있음
26-
- 본인이 선택한 러닝 코스에 대해 거리/이동시간/평균페이스 정보를 제공받을 수 있음
27-
- 미션 달성 여부에 따라 스탬프를 모아볼 수 있음
17+
## ✨ Features
2818

29-
<br>
19+
### 코스 그리기
3020

31-
<h2 id="2">🚀 Technology</h2>
21+
달리기 전, 오늘 뛰게 될 코스를 직접 그려보세요. 내가 나아갈 길을 개척하는 능동적인 러너로 거듭날 수 있습니다.
3222

33-
### 🛠 Tech Stack
23+
<img width="720" src="https://github.com/Runnect/Runnect-Android/assets/68090939/8ab5b2f9-246c-4aeb-90c8-6027ddd902da"/>
3424

35-
`Kotlin`, `JetPack`, `DataBinding`, `ViewModel`, `AAC`, `LiveData`, `OkHttp`, `Hilt`, `Glide`, `Timber`, `Coroutine`, `CustomView`
25+
### 코스 아카이빙
3626

37-
### ⚙️ Architecture
27+
내가 여태까지 그린 코스를 다시 확인하고, 스크랩 했던 공유 코스를 관리할 수 있어요. 마이페이지에서 나의 러닝 기록도 확인할 수 있어요.
3828

39-
`MVVM`
29+
<img width="720" src="https://github.com/Runnect/Runnect-Android/assets/68090939/33d01e6e-e748-4396-990a-e0feb23a3eb6"/>
4030

41-
<br>
31+
### 코스 발견
4232

43-
<h2 id="3">🏙 Result</h2>
33+
다른 러너는 어떤 코스로 달리고 있을까요? 오늘은 새롭게 발견한 코스를 스크랩하여 달려 보세요.
4434

45-
### 🎉 [Play Store Download](https://play.google.com/store/apps/details?id=com.runnect.runnect&hl=ko) (v.1.0.1)
35+
<img width="720" src="https://github.com/Runnect/Runnect-Android/assets/68090939/8f7c15ef-958f-401b-920c-cd750f4339c4"/>
4636

47-
<br>
37+
### 코스 공유
4838

49-
<p float="left">
50-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/7e268e3b-ef70-410f-9331-aa50874da7ab.jpg">
39+
코스를 보다 빠르고 간편하게 공유하세요!
5140

52-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/7cb3751a-b7f0-4c73-953c-514cad2141aa.jpg">
53-
</p>
54-
<p float="left">
55-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/f2f683bc-c9d2-4290-9e03-060f2b27b0e3.jpg">
41+
<img width="720" src="https://github.com/Runnect/Runnect-Android/assets/68090939/c05cabf1-7883-4aff-af0f-a296ceb00c80"/>
5642

57-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/5c8a226a-00d8-4d99-87af-17e2f238600b.jpg">
58-
</p>
59-
<p float="left">
60-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/d209791c-9c17-471c-b141-0f086ed6517f.jpg">
43+
## 🛠 Tech Stack
6144

62-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/f1c6bc3b-0d45-4f1f-9f8c-74b5fdc8e2ef.jpg">
63-
</p>
45+
| 구분 | 기술 스택 |
46+
| ----------------------- | ----------------------------------------------------------------------- |
47+
| Architecture | MVVM |
48+
| Design Pattern | Observer Pattern, Repository Pattern |
49+
| JetPack Components | LifeCycle, ViewModel, LiveData, DataBinding, EncryptedSharedPreferences |
50+
| Dependency Injection | Hilt |
51+
| Network | Retrofit, OkHttp, kotlinx.serialization |
52+
| Asynchronous Processing | Coroutine |
53+
| Third Party Library | Google Login, Kakao Login, Naver Map, TMAP, Glide, Coil, Timber |
54+
| Branch Strategy | Git Flow |
55+
| CI | GitHub Actions |
56+
| Data Analytics | Firebase Analytics |
57+
| Communication Tool | Notion, Slack, Figma, Postman |
6458

65-
<p float="left">
66-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/7754f0d4-bad7-4399-a3ad-e62ea22f77f6.jpg">
6759

68-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/0ccbb55d-e871-4c20-891f-a2a5f6bcac49.jpg">
69-
</p>
60+
## 🌱 Contributors
7061

71-
<p float="left">
72-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/71ef4384-0632-46ae-b765-45fe4c7fc730.jpg">
62+
### Current
7363

74-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/dc7f279b-4a6b-4e75-b3eb-979d1d7ae82e.jpg">
75-
</p>
64+
| [김우남](https://github.com/unam98) | [이하은](https://github.com/leeeha) | [백혜선](https://github.com/sxunea) | [김동현](https://github.com/Donghyeon0915) |
65+
| :---: | :---: | :---: | :---: |
66+
|<img width="150" src="https://github.com/Runnect/Runnect-Android/assets/68090939/e0fd9897-485e-41ad-b7ee-8be0573942ec">|<img width="150" src="https://github.com/Runnect/Runnect-Android/assets/68090939/9f7d8751-503c-4af6-a67c-b6e42b371e25">|<img width="150" src="https://github.com/team-winey/Winey-AOS/assets/68090939/7eb22b00-ef67-4ad0-9ae9-1bc5e579524b">|<img width="150" src="https://github.com/Runnect/Runnect-Android/assets/68090939/d9e40f14-5184-4739-82cd-0afce65553c7">|
67+
|코스 그리기, 보관함, <br> 업로드 한 코스 공유|코스 발견, 코스 상세, <br> 그린 코스 공유, 러닝 기록|유저 프로필 조회, <br> 위치 권한 설정, <br> 이벤트 태깅|보관함, 이벤트 태깅|
68+
| 2022.12 ~ ing | 2023.09 ~ ing | 2023.11 ~ ing | 2023.11 ~ ing |
7669

77-
<p float="left">
78-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/861ad768-570e-43c1-8cab-cd26a46b13ab.jpg">
70+
### Past
7971

80-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/e8566a6e-0257-412b-b770-0e37e1edd1b1.jpg">
81-
</p>
72+
| 이름 | 역할 | 활동 기간 | 작업물 |
73+
| --- | --- | --- | :----------------------: |
74+
| [박지훈](https://github.com/Larry7939) | 코스발견, 마이페이지 | 2022.12 ~ 2023.08 | [branch(23.08.27)](https://github.com/Runnect/Runnect-Android/tree/fix/215-%EC%BD%94%EC%8A%A4%EB%B0%9C%EA%B2%AC-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%ED%95%B4%EA%B2%B0) |
8275

83-
<p float="left">
84-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/66ffac66-9d0a-4bcf-a06b-534283405e4b.jpg">
85-
86-
<img width="40%" src="https://github.com/Runnect/Runnect-Android/assets/89737271/5c47b594-6f82-444b-9258-ce0882f26fa6.jpg">
87-
</p>
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+
}

0 commit comments

Comments
 (0)