Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
dce0f54
feat: 저장한 작품 더보기
ckals413 May 19, 2026
6b3c5c7
feat: 저장 작품 리스트 화면 내 뒤로가기 추가
ckals413 Jun 2, 2026
38a2aad
feat: 수정된 api 연결 (작품 총 개수 및 북마크 수 데이터 반영)
ckals413 Jun 2, 2026
5ed85fc
Merge remote-tracking branch 'origin/develop' into FLT-17-마이페이지-api-연결
ckals413 Jun 2, 2026
b773159
feat: 저장된 작품 북마크 해제 기능 구현 및 API 연동
ckals413 Jun 4, 2026
090484b
feat: 저장 작품 목록 북마크 토글 시 로딩 인디케이터 제거 및 애니메이션 추가
ckals413 Jun 4, 2026
c841cf1
feat: 저장된 작품 목록 조회 사용자 ID 연동 및 타인 프로필 조회 기능 추가
ckals413 Jun 5, 2026
1df7194
feat: 북마크 최소 개수 제한 로직을 서버 에러 응답 처리 방식으로 변경
ckals413 Jun 6, 2026
4c383a1
feat: 프로필 키워드 재계산 가능 여부 확인 및 업데이트 버튼 활성 상태 제어
ckals413 Jun 6, 2026
b4f0061
feat: 취향 키워드 재계산 기능 구현
ckals413 Jun 6, 2026
0ed097c
feat: 북마크 변경 상태 실시간 반영 및 프로필 UI 업데이트
ckals413 Jun 9, 2026
b6576a7
feat: 프로필 키워드 재계산 중 로딩 애니메이션 추가
ckals413 Jun 9, 2026
77cff3c
feat: 북마크한 콘텐츠 목록 조회 API 경로 수정 및 북마크 여부 필드 추가
ckals413 Jun 9, 2026
6d0d6e1
feat: 내 북마크 콘텐츠 조회 방식 변경 및 커서 페이지네이션 연동
ckals413 Jun 11, 2026
2cef44e
Merge branch 'develop' into FLT-17-마이페이지-api-연결
ckals413 Jun 11, 2026
d2e2e29
feat: 코드래빗 반영
ckals413 Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/src/main/java/com/flint/core/navigation/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ interface Route {
data object CollectionCreateGraph : Route

@Serializable
data object SavedContentList : Route
data class SavedContentList(
val userId: String? = null,
) : Route

@Serializable
data object AddContent : Route
Expand Down
10 changes: 7 additions & 3 deletions app/src/main/java/com/flint/data/api/ContentApi.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.flint.data.api

import com.flint.data.dto.base.BaseResponse
import com.flint.data.dto.content.response.BookmarkedContentListResponseDto
import com.flint.data.dto.content.response.MyBookmarkedContentListResponseDto
import com.flint.data.dto.ott.response.OttListResponseDto
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface ContentApi {
// 북마크한 콘텐츠 목록 조회
// 북마크한 콘텐츠 목록 조회 (커서 페이지네이션)
@GET("/api/v1/contents/bookmarks")
suspend fun getBookmarkedContentList(): BaseResponse<BookmarkedContentListResponseDto>
suspend fun getBookmarkedContentList(
@Query("cursor") cursor: String? = null,
@Query("size") size: Int = 10,
): BaseResponse<MyBookmarkedContentListResponseDto>

// 콘텐츠별 OTT 목록 조회
@GET("/api/v1/contents/ott/{contentId}")
Expand Down
14 changes: 11 additions & 3 deletions app/src/main/java/com/flint/data/api/UserApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ import com.flint.data.dto.user.response.BookmarkedCollectionListResponseDto
import com.flint.data.dto.user.response.CreatedCollectionListResponseDto
import com.flint.data.dto.user.response.UserKeywordsResponseDto
import com.flint.data.dto.user.response.UserProfileResponseDto
import retrofit2.Response
import retrofit2.http.PATCH
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query

interface UserApi {
// 내 프로필 조회 (keywordRecalculatable 포함)
@GET("/api/v1/users/me")
suspend fun getMyProfile(): BaseResponse<UserProfileResponseDto>

// 사용자 프로필 조회
@GET("/api/v1/users/{userId}")
suspend fun getUserProfile(
Expand All @@ -36,7 +42,7 @@ interface UserApi {
): BaseResponse<BookmarkedCollectionListResponseDto>

// 사용자별 북마크한 콘텐츠 목록 조회
@GET("/api/v1/contents/{userId}/bookmarked-contents")
@GET("/api/v1/users/{userId}/bookmarked-contents")
suspend fun getBookmarkedContentListByUserId(
@Path("userId") userId: String
): BaseResponse<BookmarkedContentListResponseDto>
Expand All @@ -61,8 +67,10 @@ interface UserApi {
@Path("userId") userId: String,
): BaseResponse<UserKeywordsResponseDto>

// 취향 키워드 재계산

// 취향 키워드 재계산 (응답 body에 data 필드xx)
@PATCH("/api/v1/users/me/keywords/recalculate")
suspend fun recalculateKeywords(): Response<Unit>

// 닉네임 수정
@PUT("/api/v1/users/me/nickname")
suspend fun updateNickname(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.flint.data.di.interceptor

import com.flint.data.dto.base.ErrorResponseDto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
Expand All @@ -13,6 +16,7 @@ import javax.inject.Inject

class NetworkErrorInterceptor @Inject constructor(
private val networkErrorManager: NetworkErrorManager,
private val json: Json,
) : Interceptor {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

Expand All @@ -25,13 +29,17 @@ class NetworkErrorInterceptor @Inject constructor(
if (!response.isSuccessful) {
when (response.code) {
in 300..599 -> {
scope.launch {
networkErrorManager.emitError(
NetworkError.ConnectionError(
code = response.code,
message = response.message
// errorCode 필드가 있으면 앱 레이어에서 직접 처리하는 비즈니스 에러 — 글로벌 에러 emit 생략
val isBusinessError = response.isBusinessError()
if (!isBusinessError) {
scope.launch {
networkErrorManager.emitError(
NetworkError.ConnectionError(
code = response.code,
message = response.message
)
)
)
}
}
}
}
Expand Down Expand Up @@ -61,4 +69,17 @@ class NetworkErrorInterceptor @Inject constructor(
throw e
}
}

private fun Response.isBusinessError(): Boolean {
val body = runCatching { peekBody(ERROR_BODY_PEEK_BYTES).string() }.getOrNull()
?: return false

return runCatching {
json.decodeFromString<ErrorResponseDto>(body).errorCode != null
}.getOrDefault(false)
}

private companion object {
const val ERROR_BODY_PEEK_BYTES = 64 * 1024L
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/flint/data/dto/base/ErrorResponseDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.flint.data.dto.base

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ErrorResponseDto(
@SerialName("errorCode")
val errorCode: String? = null,
@SerialName("message")
val message: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,34 @@ package com.flint.data.dto.content.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

// /api/v1/users/{userId}/bookmarked-contents 응답 (타 유저)
@Serializable
data class BookmarkedContentListResponseDto(
@SerialName("totalCount")
val totalCount: Int,
@SerialName("contents")
val contents: List<BookmarkedContentResponseDto>
)

// /api/v1/contents/bookmarks 응답 (내 프로필, 커서 페이지네이션)
@Serializable
data class MyBookmarkedContentListResponseDto(
@SerialName("data")
val data: List<BookmarkedContentResponseDto>,
@SerialName("meta")
val meta: BookmarkCursorMetaDto
)

@Serializable
data class BookmarkCursorMetaDto(
@SerialName("type")
val type: String,
@SerialName("returned")
val returned: Int,
@SerialName("nextCursor")
val nextCursor: String? = null,
)

@Serializable
data class BookmarkedContentResponseDto(
@SerialName("id")
Expand All @@ -19,6 +41,11 @@ data class BookmarkedContentResponseDto(
val year: Int,
@SerialName("imageUrl")
val imageUrl: String,
@SerialName("bookmarkCount")
val bookmarkCount: Int,
// 내 북마크 목록 응답에는 isBookmarked 없음 → 기본값 true
@SerialName("isBookmarked")
val isBookmarked: Boolean = true,
@SerialName("getOttSimpleList")
val getOttSimpleList: List<OttSimpleResponseDto>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.flint.data.dto.user.response

import com.google.gson.annotations.SerializedName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UserProfileResponseDto(
@SerializedName("id")
@SerialName("id")
val id: String,
@SerializedName("profileImageUrl")
@SerialName("profileImageUrl")
val profileImageUrl: String?,
@SerializedName("isFliner")
@SerialName("isFliner")
val isFliner: Boolean,
@SerializedName("nickname")
val nickname: String
)
@SerialName("nickname")
val nickname: String,
@SerialName("keywordRecalculatable")
val keywordRecalculatable: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.flint.domain.mapper.content

import com.flint.data.dto.content.response.BookmarkedContentListResponseDto
import com.flint.data.dto.content.response.BookmarkedContentResponseDto
import com.flint.data.dto.content.response.MyBookmarkedContentListResponseDto
import com.flint.data.dto.content.response.OttSimpleResponseDto
import com.flint.data.dto.search.SearchBookmarkedContentsResponseDto
import com.flint.domain.model.content.BookmarkedContentItemModel
Expand All @@ -10,18 +11,30 @@ import com.flint.domain.model.content.ContentModel
import com.flint.domain.type.OttType
import kotlinx.collections.immutable.toImmutableList

// /api/v1/users/{userId}/bookmarked-contents (타 유저)
fun BookmarkedContentListResponseDto.toModel() : BookmarkedContentListModel {
return BookmarkedContentListModel(
totalCount = totalCount,
contents = contents.map { it.toModel() }.toImmutableList()
)
}

// /api/v1/contents/bookmarks (내 프로필)
fun MyBookmarkedContentListResponseDto.toModel() : BookmarkedContentListModel {
return BookmarkedContentListModel(
totalCount = meta.returned,
contents = data.map { it.toModel() }.toImmutableList()
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fun BookmarkedContentResponseDto.toModel() : BookmarkedContentItemModel {
return BookmarkedContentItemModel(
id = id,
title = title,
year = year,
imageUrl = imageUrl,
bookmarkCount = bookmarkCount,
isBookmarked = isBookmarked,
getOttSimpleList = getOttSimpleList.mapNotNull { ottSimple ->
runCatching { OttType.valueOf(ottSimple.ottName) }.getOrNull()
}
Expand All @@ -39,4 +52,4 @@ fun SearchBookmarkedContentsResponseDto.toModel() : List<ContentModel> {
year = it.year
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ fun UserProfileResponseDto.toModel(): UserProfileResponseModel =
id = id,
isFliner = isFliner,
nickname = nickname,
profileImageUrl = profileImageUrl
profileImageUrl = profileImageUrl,
keywordRecalculatable = keywordRecalculatable,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.flint.domain.model.bookmark

sealed class BookmarkChange {
abstract val id: String
abstract val isBookmarked: Boolean

data class Content(
override val id: String,
override val isBookmarked: Boolean,
) : BookmarkChange()

data class Collection(
override val id: String,
override val isBookmarked: Boolean,
) : BookmarkChange()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

data class BookmarkedContentListModel(
val totalCount: Int = 0,
val contents: ImmutableList<BookmarkedContentItemModel> = persistentListOf()
) {
companion object {
val FakeList = BookmarkedContentListModel(
totalCount = 1,
contents = persistentListOf(
BookmarkedContentItemModel(
id = "0",
title = "드라마 제목",
year = 2000,
imageUrl = "",
bookmarkCount = 0,
getOttSimpleList = listOf(
OttType.Netflix,
OttType.Disney,
Expand All @@ -32,5 +35,7 @@ data class BookmarkedContentItemModel(
val title: String = "",
val year: Int = 0,
val imageUrl: String = "",
val bookmarkCount: Int = 0,
val isBookmarked: Boolean = false,
val getOttSimpleList: List<OttType> = emptyList()
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ data class UserProfileResponseModel(
val id: String,
val isFliner: Boolean,
val nickname: String,
val profileImageUrl: String?
val profileImageUrl: String?,
val keywordRecalculatable: Boolean = false,
) {
companion object {
val Empty = UserProfileResponseModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,40 @@ package com.flint.domain.repository
import com.flint.core.common.util.suspendRunCatching
import com.flint.data.api.BookmarkApi
import com.flint.domain.mapper.bookmark.toModel
import com.flint.domain.model.bookmark.BookmarkChange
import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow

@Singleton
class BookmarkRepository @Inject constructor(
private val api: BookmarkApi,
) {
private val _bookmarkChanges = MutableSharedFlow<BookmarkChange>()
val bookmarkChanges = _bookmarkChanges.asSharedFlow()

// 컬렉션 북마크 유저 조회
suspend fun getCollectionBookmarkUsers(collectionId: String): Result<CollectionBookmarkUsersModel> {
return suspendRunCatching { api.getCollectionBookmarkUsers(collectionId).data.toModel() }
}

// 컬렉션 북마크 토글
suspend fun toggleCollectionBookmark(collectionId: String): Result<Boolean> =
suspendRunCatching { api.toggleCollectionBookmark(collectionId).data }
suspend fun toggleCollectionBookmark(collectionId: String): Result<Boolean> {
val result = suspendRunCatching { api.toggleCollectionBookmark(collectionId).data }
result.getOrNull()?.let { isBookmarked ->
_bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked))
}
return result
}

// 콘텐츠 북마크 토글
suspend fun toggleContentBookmark(contentId: String): Result<Boolean> =
suspendRunCatching { api.toggleContentBookmark(contentId).data }
suspend fun toggleContentBookmark(contentId: String): Result<Boolean> {
val result = suspendRunCatching { api.toggleContentBookmark(contentId).data }
result.getOrNull()?.let { isBookmarked ->
_bookmarkChanges.emit(BookmarkChange.Content(contentId, isBookmarked))
}
return result
}
}
Loading
Loading