Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
5a35e6a
feat: 북마크한 콘텐츠 전체 개수 조회 API 변경 반영
ckals413 Jun 17, 2026
1947189
refactor: 저장 목록에서 북마크 취소 시 아이템을 삭제하지 않도록 수정
ckals413 Jun 17, 2026
589d3fe
feat: 코드리뷰 반영
ckals413 Jun 17, 2026
06fc1c3
merge - develop
ckals413 Jun 17, 2026
9b0bdf2
style: UserApi.kt 들여쓰기 수정
ckals413 Jun 17, 2026
29e988f
Merge branch 'develop' into FLT-17-마이페이지-api-연결
kimjw2003 Jun 18, 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 @@ -57,7 +57,9 @@ interface Route {
) : Route

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

@Serializable
data object AddContent : Route
Expand Down
15 changes: 12 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,24 @@
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.BookmarkCountResponseDto
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>

// 내 북마크한 콘텐츠 전체 개수 조회
@GET("/api/v1/contents/bookmarks/count")
suspend fun getBookmarkedContentCount(): BaseResponse<BookmarkCountResponseDto>

// 콘텐츠별 OTT 목록 조회
@GET("/api/v1/contents/ott/{contentId}")
Expand Down
10 changes: 9 additions & 1 deletion 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 Down Expand Up @@ -61,7 +67,9 @@ 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")
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,14 +3,41 @@ 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 = 0,
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,
)

// /api/v1/contents/bookmarks/count 응답
@Serializable
data class BookmarkCountResponseDto(
@SerialName("totalCount")
val totalCount: Int
)

@Serializable
data class BookmarkedContentResponseDto(
@SerialName("id")
Expand All @@ -22,9 +49,10 @@ data class BookmarkedContentResponseDto(
@SerialName("imageUrl")
val imageUrl: String,
@SerialName("bookmarkCount")
val bookmarkCount: Int = 0,
val bookmarkCount: Int,
// 내 북마크 목록 응답에는 isBookmarked 없음 → 기본값 true
@SerialName("isBookmarked")
val isBookmarked: Boolean = false,
val isBookmarked: Boolean = true,
@SerialName("getOttSimpleList")
val getOttSimpleList: List<OttSimpleResponseDto>
)
Expand All @@ -35,4 +63,4 @@ data class OttSimpleResponseDto(
val ottName: String,
@SerialName("logoUrl")
val logoUrl: String
)
)
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 @@ -10,8 +10,10 @@ 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()
)
}
Expand All @@ -22,6 +24,8 @@ fun BookmarkedContentResponseDto.toModel() : BookmarkedContentItemModel {
title = title,
year = year,
imageUrl = imageUrl,
bookmarkCount = bookmarkCount,
isBookmarked = isBookmarked,
getOttSimpleList = getOttSimpleList.mapNotNull { ottSimple ->
runCatching { OttType.valueOf(ottSimple.ottName) }.getOrNull()
}
Expand All @@ -39,4 +43,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
@@ -0,0 +1,6 @@
package com.flint.domain.model.bookmark

sealed class BookmarkException : Exception() {
/** 최소 저장 작품 수 제한으로 콘텐츠 북마크 해제 불가 */
object ContentMinLimitExceeded : BookmarkException()
Comment on lines +3 to +5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Throwableobject로 선언하면 예외 인스턴스가 재사용됩니다.

Line [5]의 singleton 예외는 반복 throw 시 stacktrace/내부 상태가 공유되어 디버깅 신뢰도가 떨어집니다. 예외는 매번 새 인스턴스로 생성되는 class로 바꾸는 편이 안전합니다.

🛠 제안 패치
 sealed class BookmarkException : Exception() {
     /** 최소 저장 작품 수 제한으로 콘텐츠 북마크 해제 불가 */
-    object ContentMinLimitExceeded : BookmarkException()
+    class ContentMinLimitExceeded : BookmarkException()
 }
// throw 지점 예시 (BookmarkRepository)
throw BookmarkException.ContentMinLimitExceeded()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/flint/domain/model/bookmark/BookmarkException.kt`
around lines 3 - 5, The ContentMinLimitExceeded exception is declared as a
singleton object, which causes the exception instance to be reused across
multiple throws, leading to shared stacktraces and internal state that degrades
debugging reliability. Change ContentMinLimitExceeded from an object declaration
to a class declaration so that a new exception instance is created each time it
is thrown. This ensures each thrown exception has its own independent stacktrace
and state for proper error tracking and debugging.

}
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 @@ -2,23 +2,60 @@ package com.flint.domain.repository

import com.flint.core.common.util.suspendRunCatching
import com.flint.data.api.BookmarkApi
import com.flint.data.dto.base.ErrorResponseDto
import com.flint.domain.mapper.bookmark.toModel
import com.flint.domain.model.bookmark.BookmarkChange
import com.flint.domain.model.bookmark.BookmarkException
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
import kotlinx.serialization.json.Json
import retrofit2.HttpException

@Singleton
class BookmarkRepository @Inject constructor(
private val api: BookmarkApi,
private val json: Json,
) {
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 }
// 최소 저장 수 제한 초과 시 BookmarkException.ContentMinLimitExceeded 반환
suspend fun toggleContentBookmark(contentId: String): Result<Boolean> {
val result = suspendRunCatching {
try {
api.toggleContentBookmark(contentId).data
} catch (e: HttpException) {
val errorCode = e.response()?.errorBody()?.string()
?.let { runCatching { json.decodeFromString(ErrorResponseDto.serializer(), it).errorCode }.getOrNull() }
if (errorCode == CONTENT_MIN_LIMIT_ERROR_CODE) throw BookmarkException.ContentMinLimitExceeded
throw e
}
}
result.getOrNull()?.let { isBookmarked ->
_bookmarkChanges.emit(BookmarkChange.Content(contentId, isBookmarked))
}
return result
}

private companion object {
const val CONTENT_MIN_LIMIT_ERROR_CODE = "BOOKMARK.CONTENT_MIN_LIMIT"
}
}
Loading
Loading