Skip to content

Commit 03dee6f

Browse files
authored
Merge pull request #82 from YAPP-Github/feat/#77-pose-api
[feat] #77 포즈 API 연동 및 랜덤 포즈 추천 구현
2 parents a24bbbf + 438bff0 commit 03dee6f

28 files changed

Lines changed: 628 additions & 182 deletions

File tree

core/data-api/src/main/java/com/neki/android/core/dataapi/repository/PoseRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ interface PoseRepository {
1313
sortOrder: SortOrder = SortOrder.DESC,
1414
): Flow<PagingData<Pose>>
1515

16+
fun getScrappedPosesFlow(
17+
sortOrder: SortOrder = SortOrder.DESC,
18+
): Flow<PagingData<Pose>>
19+
1620
suspend fun getPose(poseId: Long): Result<Pose>
1721

1822
suspend fun getRandomPose(headCount: PeopleCount): Result<Pose>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.neki.android.core.data.paging
2+
3+
import androidx.paging.PagingSource
4+
import androidx.paging.PagingState
5+
import com.neki.android.core.data.remote.api.PoseService
6+
import com.neki.android.core.model.Pose
7+
import com.neki.android.core.model.SortOrder
8+
9+
class ScrapPosePagingSource(
10+
private val poseService: PoseService,
11+
private val sortOrder: SortOrder,
12+
) : PagingSource<Int, Pose>() {
13+
14+
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Pose> {
15+
return try {
16+
val page = params.key ?: 0
17+
val response = poseService.getScrappedPoses(
18+
page = page,
19+
size = params.loadSize,
20+
sortOrder = sortOrder.name,
21+
)
22+
val poses = response.data.toModels()
23+
24+
LoadResult.Page(
25+
data = poses,
26+
prevKey = if (page == 0) null else page - 1,
27+
nextKey = if (poses.isEmpty()) null else page + 1,
28+
)
29+
} catch (e: Exception) {
30+
LoadResult.Error(e)
31+
}
32+
}
33+
34+
override fun getRefreshKey(state: PagingState<Int, Pose>): Int? {
35+
return state.anchorPosition?.let { anchorPosition ->
36+
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
37+
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
38+
}
39+
}
40+
}

core/data/src/main/java/com/neki/android/core/data/remote/api/PoseService.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.neki.android.core.data.remote.model.response.BasicNullableResponse
55
import com.neki.android.core.data.remote.model.response.BasicResponse
66
import com.neki.android.core.data.remote.model.response.PoseDetailResponse
77
import com.neki.android.core.data.remote.model.response.PoseResponse
8+
import com.neki.android.core.data.remote.model.response.ScrappedPoseResponse
89
import io.ktor.client.HttpClient
910
import io.ktor.client.call.body
1011
import io.ktor.client.request.get
@@ -43,6 +44,19 @@ class PoseService @Inject constructor(
4344
}.body()
4445
}
4546

47+
// 스크랩된 포즈 목록 조회
48+
suspend fun getScrappedPoses(
49+
page: Int = 0,
50+
size: Int = 20,
51+
sortOrder: String = "DESC",
52+
): BasicResponse<ScrappedPoseResponse> {
53+
return client.get("/api/poses/scrap") {
54+
parameter("page", page)
55+
parameter("size", size)
56+
parameter("sortOrder", sortOrder)
57+
}.body()
58+
}
59+
4660
// 스크랩 업데이트
4761
suspend fun updateScrap(poseId: Long, scrap: Boolean): BasicNullableResponse<Unit> {
4862
return client.patch("/api/poses/$poseId/scrap") {

core/data/src/main/java/com/neki/android/core/data/remote/model/response/PoseResponse.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ data class PoseResponse(
2222
id = poseId,
2323
poseImageUrl = imageUrl,
2424
peopleCount = PeopleCount.entries.find { it.name == headCount }?.value ?: 1,
25+
isScrapped = false,
26+
)
27+
}
28+
29+
fun toModels() = items.map { it.toModel() }
30+
}
31+
32+
@Serializable
33+
data class ScrappedPoseResponse(
34+
@SerialName("hasNext") val hasNext: Boolean,
35+
@SerialName("items") val items: List<Item>,
36+
) {
37+
@Serializable
38+
data class Item(
39+
@SerialName("poseId") val poseId: Long,
40+
@SerialName("headCount") val headCount: String,
41+
@SerialName("imageUrl") val imageUrl: String,
42+
@SerialName("contentType") val contentType: String,
43+
@SerialName("createdAt") val createdAt: String,
44+
) {
45+
internal fun toModel() = Pose(
46+
id = poseId,
47+
poseImageUrl = imageUrl,
48+
peopleCount = PeopleCount.entries.find { it.name == headCount }?.value ?: 1,
49+
isScrapped = true,
2550
)
2651
}
2752

core/data/src/main/java/com/neki/android/core/data/repository/impl/PoseRepositoryImpl.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.paging.Pager
44
import androidx.paging.PagingConfig
55
import androidx.paging.PagingData
66
import com.neki.android.core.data.paging.PosePagingSource
7+
import com.neki.android.core.data.paging.ScrapPosePagingSource
78
import com.neki.android.core.data.remote.api.PoseService
89
import com.neki.android.core.data.util.runSuspendCatching
910
import com.neki.android.core.dataapi.repository.PoseRepository
@@ -41,6 +42,25 @@ class PoseRepositoryImpl @Inject constructor(
4142
).flow
4243
}
4344

45+
override fun getScrappedPosesFlow(
46+
sortOrder: SortOrder,
47+
): Flow<PagingData<Pose>> {
48+
return Pager(
49+
config = PagingConfig(
50+
pageSize = PAGE_SIZE,
51+
initialLoadSize = PAGE_SIZE,
52+
prefetchDistance = PREFETCH_DISTANCE,
53+
enablePlaceholders = false,
54+
),
55+
pagingSourceFactory = {
56+
ScrapPosePagingSource(
57+
poseService = poseService,
58+
sortOrder = sortOrder,
59+
)
60+
},
61+
).flow
62+
}
63+
4464
override suspend fun getPose(poseId: Long): Result<Pose> = runSuspendCatching {
4565
poseService.getPose(poseId).data.toModel()
4666
}

core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
44
import androidx.compose.foundation.shape.RoundedCornerShape
55
import androidx.compose.runtime.Composable
66
import androidx.compose.ui.Modifier
7+
import androidx.compose.ui.draw.alpha
78
import androidx.compose.ui.geometry.Offset
89
import androidx.compose.ui.graphics.Brush
910
import androidx.compose.ui.graphics.Color
@@ -35,6 +36,22 @@ fun Modifier.photoBackground(
3536
shape = shape,
3637
)
3738

39+
/**
40+
* 포즈 컴포넌트에 적용되는 그라데이션 배경
41+
* 상단에서 134/242 지점까지 어두워지는 효과
42+
*/
43+
fun Modifier.poseBackground(
44+
shape: Shape = RoundedCornerShape(12.dp),
45+
): Modifier = this.background(
46+
brush = Brush.verticalGradient(
47+
colorStops = arrayOf(
48+
0f to Color.Black.copy(alpha = 0.2f),
49+
134f / 242f to Color.Black.copy(alpha = 0f),
50+
),
51+
),
52+
shape = shape,
53+
)
54+
3855
/**
3956
* 블러 효과가 적용된 배경을 설정하는 Modifier 확장 함수
4057
*

feature/pose/impl/src/main/res/drawable/icon_arrow_top_right.xml renamed to core/designsystem/src/main/res/drawable/icon_arrow_top_right.xml

File renamed without changes.

feature/pose/impl/src/main/res/drawable/icon_repeat_recommendation.xml renamed to core/designsystem/src/main/res/drawable/icon_repeat_recommendation.xml

File renamed without changes.

feature/pose/impl/src/main/res/drawable/ic_scrap_selected.xml renamed to core/designsystem/src/main/res/drawable/icon_scrap.xml

File renamed without changes.

feature/pose/impl/src/main/res/drawable/icon_scrap_unselected.xml renamed to core/designsystem/src/main/res/drawable/icon_scrap_unselected.xml

File renamed without changes.

0 commit comments

Comments
 (0)