diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4a8e59719..a31db4508 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,9 @@ android:theme="@style/Theme.Turip" android:usesCleartextTraffic="true" tools:targetApi="31"> + diff --git a/android/app/src/main/java/com/on/turip/data/account/AccountMapper.kt b/android/app/src/main/java/com/on/turip/data/account/AccountMapper.kt new file mode 100644 index 000000000..c0ad6e7cf --- /dev/null +++ b/android/app/src/main/java/com/on/turip/data/account/AccountMapper.kt @@ -0,0 +1,12 @@ +package com.on.turip.data.account + +import com.on.turip.data.account.dto.MyProfileResponse +import com.on.turip.domain.account.Account +import com.on.turip.domain.account.Role + +fun MyProfileResponse.toDomain(): Account = + Account( + id = id, + nickname = nickname, + role = Role.from(role), + ) diff --git a/android/app/src/main/java/com/on/turip/data/accounts/datasource/AccountRemoteDataSource.kt b/android/app/src/main/java/com/on/turip/data/account/datasource/AccountRemoteDataSource.kt similarity index 59% rename from android/app/src/main/java/com/on/turip/data/accounts/datasource/AccountRemoteDataSource.kt rename to android/app/src/main/java/com/on/turip/data/account/datasource/AccountRemoteDataSource.kt index 86e924419..834cc77dd 100644 --- a/android/app/src/main/java/com/on/turip/data/accounts/datasource/AccountRemoteDataSource.kt +++ b/android/app/src/main/java/com/on/turip/data/account/datasource/AccountRemoteDataSource.kt @@ -1,7 +1,7 @@ -package com.on.turip.data.accounts.datasource +package com.on.turip.data.account.datasource import com.on.turip.core.result.TuripResult -import com.on.turip.data.accounts.dto.MyProfileResponse +import com.on.turip.data.account.dto.MyProfileResponse interface AccountRemoteDataSource { suspend fun getMyProfile(): TuripResult diff --git a/android/app/src/main/java/com/on/turip/data/accounts/datasource/DefaultAccountRemoteDataSource.kt b/android/app/src/main/java/com/on/turip/data/account/datasource/DefaultAccountRemoteDataSource.kt similarity index 70% rename from android/app/src/main/java/com/on/turip/data/accounts/datasource/DefaultAccountRemoteDataSource.kt rename to android/app/src/main/java/com/on/turip/data/account/datasource/DefaultAccountRemoteDataSource.kt index ce4675f27..2e316309e 100644 --- a/android/app/src/main/java/com/on/turip/data/accounts/datasource/DefaultAccountRemoteDataSource.kt +++ b/android/app/src/main/java/com/on/turip/data/account/datasource/DefaultAccountRemoteDataSource.kt @@ -1,8 +1,8 @@ -package com.on.turip.data.accounts.datasource +package com.on.turip.data.account.datasource import com.on.turip.core.result.TuripResult -import com.on.turip.data.accounts.dto.MyProfileResponse -import com.on.turip.data.accounts.service.AccountService +import com.on.turip.data.account.dto.MyProfileResponse +import com.on.turip.data.account.service.AccountService import com.on.turip.data.result.safeApiCall import javax.inject.Inject diff --git a/android/app/src/main/java/com/on/turip/data/accounts/dto/MyProfileResponse.kt b/android/app/src/main/java/com/on/turip/data/account/dto/MyProfileResponse.kt similarity index 87% rename from android/app/src/main/java/com/on/turip/data/accounts/dto/MyProfileResponse.kt rename to android/app/src/main/java/com/on/turip/data/account/dto/MyProfileResponse.kt index 6d72d5375..5dd0dc242 100644 --- a/android/app/src/main/java/com/on/turip/data/accounts/dto/MyProfileResponse.kt +++ b/android/app/src/main/java/com/on/turip/data/account/dto/MyProfileResponse.kt @@ -1,4 +1,4 @@ -package com.on.turip.data.accounts.dto +package com.on.turip.data.account.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/com/on/turip/data/accounts/repository/DefaultAccountRepository.kt b/android/app/src/main/java/com/on/turip/data/account/repository/DefaultAccountRepository.kt similarity index 60% rename from android/app/src/main/java/com/on/turip/data/accounts/repository/DefaultAccountRepository.kt rename to android/app/src/main/java/com/on/turip/data/account/repository/DefaultAccountRepository.kt index 7001dfd19..e72edd64d 100644 --- a/android/app/src/main/java/com/on/turip/data/accounts/repository/DefaultAccountRepository.kt +++ b/android/app/src/main/java/com/on/turip/data/account/repository/DefaultAccountRepository.kt @@ -1,11 +1,11 @@ -package com.on.turip.data.accounts.repository +package com.on.turip.data.account.repository import com.on.turip.core.result.TuripResult import com.on.turip.core.result.mapCatching -import com.on.turip.data.accounts.datasource.AccountRemoteDataSource -import com.on.turip.data.accounts.toDomain -import com.on.turip.domain.accounts.Account -import com.on.turip.domain.accounts.AccountRepository +import com.on.turip.data.account.datasource.AccountRemoteDataSource +import com.on.turip.data.account.toDomain +import com.on.turip.domain.account.Account +import com.on.turip.domain.account.AccountRepository import javax.inject.Inject class DefaultAccountRepository @Inject constructor( diff --git a/android/app/src/main/java/com/on/turip/data/accounts/service/AccountService.kt b/android/app/src/main/java/com/on/turip/data/account/service/AccountService.kt similarity index 59% rename from android/app/src/main/java/com/on/turip/data/accounts/service/AccountService.kt rename to android/app/src/main/java/com/on/turip/data/account/service/AccountService.kt index 07dc89db9..1e3d70819 100644 --- a/android/app/src/main/java/com/on/turip/data/accounts/service/AccountService.kt +++ b/android/app/src/main/java/com/on/turip/data/account/service/AccountService.kt @@ -1,6 +1,6 @@ -package com.on.turip.data.accounts.service +package com.on.turip.data.account.service -import com.on.turip.data.accounts.dto.MyProfileResponse +import com.on.turip.data.account.dto.MyProfileResponse import de.jensklingenberg.ktorfit.http.GET interface AccountService { diff --git a/android/app/src/main/java/com/on/turip/data/accounts/AccountMapper.kt b/android/app/src/main/java/com/on/turip/data/accounts/AccountMapper.kt deleted file mode 100644 index 6c4c6fec2..000000000 --- a/android/app/src/main/java/com/on/turip/data/accounts/AccountMapper.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.on.turip.data.accounts - -import com.on.turip.data.accounts.dto.MyProfileResponse -import com.on.turip.domain.accounts.Account -import com.on.turip.domain.accounts.Role - -fun MyProfileResponse.toDomain(): Account = - Account( - id = id, - nickname = nickname, - role = Role.from(role), - ) diff --git a/android/app/src/main/java/com/on/turip/data/bookmark/BookmarkMapper.kt b/android/app/src/main/java/com/on/turip/data/bookmark/BookmarkMapper.kt new file mode 100644 index 000000000..31f0d7eae --- /dev/null +++ b/android/app/src/main/java/com/on/turip/data/bookmark/BookmarkMapper.kt @@ -0,0 +1,23 @@ +package com.on.turip.data.bookmark + +import com.on.turip.data.bookmark.dto.BookmarkAddRequest +import com.on.turip.data.bookmark.dto.BookmarkContentResponse +import com.on.turip.data.bookmark.dto.BookmarkContentsResponse +import com.on.turip.data.content.toDomain +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.common.paging.Page + +fun Long.toRequestDto(): BookmarkAddRequest = BookmarkAddRequest(contentId = this) + +fun BookmarkContentsResponse.toDomain(): Page = + Page( + items = contents.map { it.toDomain() }, + hasNext = loadable, + ) + +fun BookmarkContentResponse.toDomain(): BookmarkContent = + BookmarkContent( + content = content.toDomain(), + tripDuration = tripDuration.toDomain(), + tripPlaceCount = tripPlaceCount, + ) diff --git a/android/app/src/main/java/com/on/turip/data/bookmark/datasource/BookmarkRemoteDataSource.kt b/android/app/src/main/java/com/on/turip/data/bookmark/datasource/BookmarkRemoteDataSource.kt new file mode 100644 index 000000000..a62f7af36 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/data/bookmark/datasource/BookmarkRemoteDataSource.kt @@ -0,0 +1,17 @@ +package com.on.turip.data.bookmark.datasource + +import com.on.turip.core.result.TuripResult +import com.on.turip.data.bookmark.dto.BookmarkAddRequest +import com.on.turip.data.bookmark.dto.BookmarkContentsResponse +import com.on.turip.data.bookmark.dto.BookmarkCountResponse +import com.on.turip.domain.common.paging.Cursor + +interface BookmarkRemoteDataSource { + suspend fun postBookmark(bookmarkAddRequest: BookmarkAddRequest): TuripResult + + suspend fun deleteBookmark(contentId: Long): TuripResult + + suspend fun getBookmarks(cursor: Cursor): TuripResult + + suspend fun getBookmarkCount(): TuripResult +} diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/datasource/DefaultBookmarkRemoteDataSource.kt b/android/app/src/main/java/com/on/turip/data/bookmark/datasource/DefaultBookmarkRemoteDataSource.kt similarity index 56% rename from android/app/src/main/java/com/on/turip/data/bookmarks/datasource/DefaultBookmarkRemoteDataSource.kt rename to android/app/src/main/java/com/on/turip/data/bookmark/datasource/DefaultBookmarkRemoteDataSource.kt index 7424da24d..c8b8ef50e 100644 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/datasource/DefaultBookmarkRemoteDataSource.kt +++ b/android/app/src/main/java/com/on/turip/data/bookmark/datasource/DefaultBookmarkRemoteDataSource.kt @@ -1,10 +1,12 @@ -package com.on.turip.data.bookmarks.datasource +package com.on.turip.data.bookmark.datasource import com.on.turip.core.result.TuripResult -import com.on.turip.data.bookmarks.dto.BookmarkAddRequest -import com.on.turip.data.bookmarks.dto.BookmarkContentsResponse -import com.on.turip.data.bookmarks.service.BookmarkService +import com.on.turip.data.bookmark.dto.BookmarkAddRequest +import com.on.turip.data.bookmark.dto.BookmarkContentsResponse +import com.on.turip.data.bookmark.dto.BookmarkCountResponse +import com.on.turip.data.bookmark.service.BookmarkService import com.on.turip.data.result.safeApiCall +import com.on.turip.domain.common.paging.Cursor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject @@ -24,11 +26,13 @@ class DefaultBookmarkRemoteDataSource @Inject constructor( safeApiCall { bookmarkService.deleteBookmark(contentId) } } - override suspend fun getBookmarks( - size: Int, - lastId: Long, - ): TuripResult = + override suspend fun getBookmarks(cursor: Cursor): TuripResult = withContext(coroutineContext) { - safeApiCall { bookmarkService.getBookmarks(size, lastId) } + safeApiCall { bookmarkService.getBookmarks(cursor.size, cursor.lastId ?: 0L) } + } + + override suspend fun getBookmarkCount(): TuripResult = + withContext(coroutineContext) { + safeApiCall { bookmarkService.getBookmarkCount() } } } diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkAddRequest.kt b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkAddRequest.kt similarity index 82% rename from android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkAddRequest.kt rename to android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkAddRequest.kt index a49882d66..5a6680200 100644 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkAddRequest.kt +++ b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkAddRequest.kt @@ -1,4 +1,4 @@ -package com.on.turip.data.bookmarks.dto +package com.on.turip.data.bookmark.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkContentResponse.kt b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkContentResponse.kt similarity index 92% rename from android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkContentResponse.kt rename to android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkContentResponse.kt index b46a6b114..1a43db99a 100644 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkContentResponse.kt +++ b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkContentResponse.kt @@ -1,4 +1,4 @@ -package com.on.turip.data.bookmarks.dto +package com.on.turip.data.bookmark.dto import com.on.turip.data.content.dto.ContentResponse import com.on.turip.data.content.dto.TripDurationInformationResponse diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkContentsResponse.kt b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkContentsResponse.kt similarity index 87% rename from android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkContentsResponse.kt rename to android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkContentsResponse.kt index 139ca4aa9..fa86764f0 100644 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/dto/BookmarkContentsResponse.kt +++ b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkContentsResponse.kt @@ -1,4 +1,4 @@ -package com.on.turip.data.bookmarks.dto +package com.on.turip.data.bookmark.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkCountResponse.kt b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkCountResponse.kt new file mode 100644 index 000000000..2a76a8ba8 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/data/bookmark/dto/BookmarkCountResponse.kt @@ -0,0 +1,10 @@ +package com.on.turip.data.bookmark.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BookmarkCountResponse( + @SerialName("count") + val count: Int, +) diff --git a/android/app/src/main/java/com/on/turip/data/bookmark/repository/DefaultBookmarkRepository.kt b/android/app/src/main/java/com/on/turip/data/bookmark/repository/DefaultBookmarkRepository.kt new file mode 100644 index 000000000..e3b9b247f --- /dev/null +++ b/android/app/src/main/java/com/on/turip/data/bookmark/repository/DefaultBookmarkRepository.kt @@ -0,0 +1,26 @@ +package com.on.turip.data.bookmark.repository + +import com.on.turip.core.result.TuripResult +import com.on.turip.core.result.mapCatching +import com.on.turip.data.bookmark.datasource.BookmarkRemoteDataSource +import com.on.turip.data.bookmark.toDomain +import com.on.turip.data.bookmark.toRequestDto +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.bookmark.repository.BookmarkRepository +import com.on.turip.domain.common.paging.Cursor +import com.on.turip.domain.common.paging.Page +import javax.inject.Inject + +class DefaultBookmarkRepository @Inject constructor( + private val bookmarkRemoteDataSource: BookmarkRemoteDataSource, +) : BookmarkRepository { + override suspend fun createBookmark(contentId: Long): TuripResult = + bookmarkRemoteDataSource.postBookmark(contentId.toRequestDto()) + + override suspend fun deleteBookmark(contentId: Long): TuripResult = bookmarkRemoteDataSource.deleteBookmark(contentId) + + override suspend fun loadBookmarks(cursor: Cursor): TuripResult> = + bookmarkRemoteDataSource.getBookmarks(cursor).mapCatching { it.toDomain() } + + override suspend fun loadBookmarkCount(): TuripResult = bookmarkRemoteDataSource.getBookmarkCount().mapCatching { it.count } +} diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/service/BookmarkService.kt b/android/app/src/main/java/com/on/turip/data/bookmark/service/BookmarkService.kt similarity index 67% rename from android/app/src/main/java/com/on/turip/data/bookmarks/service/BookmarkService.kt rename to android/app/src/main/java/com/on/turip/data/bookmark/service/BookmarkService.kt index 81bfce88c..d780c69b2 100644 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/service/BookmarkService.kt +++ b/android/app/src/main/java/com/on/turip/data/bookmark/service/BookmarkService.kt @@ -1,7 +1,8 @@ -package com.on.turip.data.bookmarks.service +package com.on.turip.data.bookmark.service -import com.on.turip.data.bookmarks.dto.BookmarkAddRequest -import com.on.turip.data.bookmarks.dto.BookmarkContentsResponse +import com.on.turip.data.bookmark.dto.BookmarkAddRequest +import com.on.turip.data.bookmark.dto.BookmarkContentsResponse +import com.on.turip.data.bookmark.dto.BookmarkCountResponse import de.jensklingenberg.ktorfit.http.Body import de.jensklingenberg.ktorfit.http.DELETE import de.jensklingenberg.ktorfit.http.GET @@ -24,4 +25,7 @@ interface BookmarkService { @Query("size") size: Int, @Query("lastId") lastId: Long, ): BookmarkContentsResponse + + @GET("bookmarks/count") + suspend fun getBookmarkCount(): BookmarkCountResponse } diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/BookmarkMapper.kt b/android/app/src/main/java/com/on/turip/data/bookmarks/BookmarkMapper.kt deleted file mode 100644 index b5023c6e9..000000000 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/BookmarkMapper.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.on.turip.data.bookmarks - -import com.on.turip.data.bookmarks.dto.BookmarkAddRequest -import com.on.turip.data.bookmarks.dto.BookmarkContentResponse -import com.on.turip.data.bookmarks.dto.BookmarkContentsResponse -import com.on.turip.data.content.toDomain -import com.on.turip.domain.bookmark.BookmarkContent -import com.on.turip.domain.bookmark.PagedBookmarkContents - -fun Long.toRequestDto(): BookmarkAddRequest = BookmarkAddRequest(contentId = this) - -fun BookmarkContentsResponse.toDomain(): PagedBookmarkContents = - PagedBookmarkContents( - bookmarkContents = contents.map { it.toDomain() }, - loadable = loadable, - ) - -fun BookmarkContentResponse.toDomain(): BookmarkContent = - BookmarkContent( - content = content.toDomain(), - tripDuration = tripDuration.toDomain(), - tripPlaceCount = tripPlaceCount, - ) diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/datasource/BookmarkRemoteDataSource.kt b/android/app/src/main/java/com/on/turip/data/bookmarks/datasource/BookmarkRemoteDataSource.kt deleted file mode 100644 index a60455a0e..000000000 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/datasource/BookmarkRemoteDataSource.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.on.turip.data.bookmarks.datasource - -import com.on.turip.core.result.TuripResult -import com.on.turip.data.bookmarks.dto.BookmarkAddRequest -import com.on.turip.data.bookmarks.dto.BookmarkContentsResponse - -interface BookmarkRemoteDataSource { - suspend fun postBookmark(bookmarkAddRequest: BookmarkAddRequest): TuripResult - - suspend fun deleteBookmark(contentId: Long): TuripResult - - suspend fun getBookmarks( - size: Int, - lastId: Long, - ): TuripResult -} diff --git a/android/app/src/main/java/com/on/turip/data/bookmarks/repository/DefaultBookmarkRepository.kt b/android/app/src/main/java/com/on/turip/data/bookmarks/repository/DefaultBookmarkRepository.kt deleted file mode 100644 index a9d101b90..000000000 --- a/android/app/src/main/java/com/on/turip/data/bookmarks/repository/DefaultBookmarkRepository.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.on.turip.data.bookmarks.repository - -import com.on.turip.core.result.TuripResult -import com.on.turip.core.result.mapCatching -import com.on.turip.data.bookmarks.datasource.BookmarkRemoteDataSource -import com.on.turip.data.bookmarks.toDomain -import com.on.turip.data.bookmarks.toRequestDto -import com.on.turip.domain.bookmark.PagedBookmarkContents -import com.on.turip.domain.bookmark.repository.BookmarkRepository -import javax.inject.Inject - -class DefaultBookmarkRepository @Inject constructor( - private val bookmarkRemoteDataSource: BookmarkRemoteDataSource, -) : BookmarkRepository { - override suspend fun createBookmark(contentId: Long): TuripResult = - bookmarkRemoteDataSource.postBookmark(contentId.toRequestDto()) - - override suspend fun deleteBookmark(contentId: Long): TuripResult = bookmarkRemoteDataSource.deleteBookmark(contentId) - - override suspend fun loadBookmarks( - size: Int, - lastId: Long, - ): TuripResult = bookmarkRemoteDataSource.getBookmarks(size, lastId).mapCatching { it.toDomain() } -} diff --git a/android/app/src/main/java/com/on/turip/di/DataSourceModule.kt b/android/app/src/main/java/com/on/turip/di/DataSourceModule.kt index 732494988..5450b0b9e 100644 --- a/android/app/src/main/java/com/on/turip/di/DataSourceModule.kt +++ b/android/app/src/main/java/com/on/turip/di/DataSourceModule.kt @@ -1,9 +1,9 @@ package com.on.turip.di -import com.on.turip.data.accounts.datasource.AccountRemoteDataSource -import com.on.turip.data.accounts.datasource.DefaultAccountRemoteDataSource -import com.on.turip.data.bookmarks.datasource.BookmarkRemoteDataSource -import com.on.turip.data.bookmarks.datasource.DefaultBookmarkRemoteDataSource +import com.on.turip.data.account.datasource.AccountRemoteDataSource +import com.on.turip.data.account.datasource.DefaultAccountRemoteDataSource +import com.on.turip.data.bookmark.datasource.BookmarkRemoteDataSource +import com.on.turip.data.bookmark.datasource.DefaultBookmarkRemoteDataSource import com.on.turip.data.content.datasource.ContentRemoteDataSource import com.on.turip.data.content.datasource.DefaultContentRemoteDataSource import com.on.turip.data.login.datasource.AuthDataSource diff --git a/android/app/src/main/java/com/on/turip/di/RepositoryModule.kt b/android/app/src/main/java/com/on/turip/di/RepositoryModule.kt index 1735a5499..6a045ca08 100644 --- a/android/app/src/main/java/com/on/turip/di/RepositoryModule.kt +++ b/android/app/src/main/java/com/on/turip/di/RepositoryModule.kt @@ -1,7 +1,7 @@ package com.on.turip.di -import com.on.turip.data.accounts.repository.DefaultAccountRepository -import com.on.turip.data.bookmarks.repository.DefaultBookmarkRepository +import com.on.turip.data.account.repository.DefaultAccountRepository +import com.on.turip.data.bookmark.repository.DefaultBookmarkRepository import com.on.turip.data.content.repository.DefaultContentRepository import com.on.turip.data.login.repository.DefaultAuthRepository import com.on.turip.data.login.repository.DefaultGuestRepository @@ -10,7 +10,7 @@ import com.on.turip.data.region.repository.DefaultRegionRepository import com.on.turip.data.searchhistory.repository.DefaultSearchHistoryRepository import com.on.turip.data.turip.repository.DefaultTuripRepository import com.on.turip.data.userstorage.repository.DefaultUserStorageRepository -import com.on.turip.domain.accounts.AccountRepository +import com.on.turip.domain.account.AccountRepository import com.on.turip.domain.bookmark.repository.BookmarkRepository import com.on.turip.domain.content.repository.ContentRepository import com.on.turip.domain.login.AuthRepository diff --git a/android/app/src/main/java/com/on/turip/di/ServiceModule.kt b/android/app/src/main/java/com/on/turip/di/ServiceModule.kt index 27f09152c..8cede2f3c 100644 --- a/android/app/src/main/java/com/on/turip/di/ServiceModule.kt +++ b/android/app/src/main/java/com/on/turip/di/ServiceModule.kt @@ -1,9 +1,9 @@ package com.on.turip.di -import com.on.turip.data.accounts.service.AccountService -import com.on.turip.data.accounts.service.createAccountService -import com.on.turip.data.bookmarks.service.BookmarkService -import com.on.turip.data.bookmarks.service.createBookmarkService +import com.on.turip.data.account.service.AccountService +import com.on.turip.data.account.service.createAccountService +import com.on.turip.data.bookmark.service.BookmarkService +import com.on.turip.data.bookmark.service.createBookmarkService import com.on.turip.data.content.service.ContentService import com.on.turip.data.content.service.createContentService import com.on.turip.data.login.service.AuthService diff --git a/android/app/src/main/java/com/on/turip/domain/accounts/Account.kt b/android/app/src/main/java/com/on/turip/domain/account/Account.kt similarity index 70% rename from android/app/src/main/java/com/on/turip/domain/accounts/Account.kt rename to android/app/src/main/java/com/on/turip/domain/account/Account.kt index bdb9c1306..106d1cea6 100644 --- a/android/app/src/main/java/com/on/turip/domain/accounts/Account.kt +++ b/android/app/src/main/java/com/on/turip/domain/account/Account.kt @@ -1,4 +1,4 @@ -package com.on.turip.domain.accounts +package com.on.turip.domain.account data class Account( val id: Long, diff --git a/android/app/src/main/java/com/on/turip/domain/accounts/AccountRepository.kt b/android/app/src/main/java/com/on/turip/domain/account/AccountRepository.kt similarity index 78% rename from android/app/src/main/java/com/on/turip/domain/accounts/AccountRepository.kt rename to android/app/src/main/java/com/on/turip/domain/account/AccountRepository.kt index cf314859a..021ccbe4c 100644 --- a/android/app/src/main/java/com/on/turip/domain/accounts/AccountRepository.kt +++ b/android/app/src/main/java/com/on/turip/domain/account/AccountRepository.kt @@ -1,4 +1,4 @@ -package com.on.turip.domain.accounts +package com.on.turip.domain.account import com.on.turip.core.result.TuripResult diff --git a/android/app/src/main/java/com/on/turip/domain/accounts/Role.kt b/android/app/src/main/java/com/on/turip/domain/account/Role.kt similarity index 88% rename from android/app/src/main/java/com/on/turip/domain/accounts/Role.kt rename to android/app/src/main/java/com/on/turip/domain/account/Role.kt index 49e5bca64..b30725564 100644 --- a/android/app/src/main/java/com/on/turip/domain/accounts/Role.kt +++ b/android/app/src/main/java/com/on/turip/domain/account/Role.kt @@ -1,4 +1,4 @@ -package com.on.turip.domain.accounts +package com.on.turip.domain.account enum class Role( private val tag: String, diff --git a/android/app/src/main/java/com/on/turip/domain/bookmark/PagedBookmarkContents.kt b/android/app/src/main/java/com/on/turip/domain/bookmark/PagedBookmarkContents.kt deleted file mode 100644 index 3509010d9..000000000 --- a/android/app/src/main/java/com/on/turip/domain/bookmark/PagedBookmarkContents.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.on.turip.domain.bookmark - -data class PagedBookmarkContents( - val bookmarkContents: List, - val loadable: Boolean, -) diff --git a/android/app/src/main/java/com/on/turip/domain/bookmark/repository/BookmarkRepository.kt b/android/app/src/main/java/com/on/turip/domain/bookmark/repository/BookmarkRepository.kt index 256caa5a5..9d56f7bdf 100644 --- a/android/app/src/main/java/com/on/turip/domain/bookmark/repository/BookmarkRepository.kt +++ b/android/app/src/main/java/com/on/turip/domain/bookmark/repository/BookmarkRepository.kt @@ -1,15 +1,16 @@ package com.on.turip.domain.bookmark.repository import com.on.turip.core.result.TuripResult -import com.on.turip.domain.bookmark.PagedBookmarkContents +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.common.paging.Cursor +import com.on.turip.domain.common.paging.Page interface BookmarkRepository { suspend fun createBookmark(contentId: Long): TuripResult suspend fun deleteBookmark(contentId: Long): TuripResult - suspend fun loadBookmarks( - size: Int, - lastId: Long, - ): TuripResult + suspend fun loadBookmarks(cursor: Cursor): TuripResult> + + suspend fun loadBookmarkCount(): TuripResult } diff --git a/android/app/src/main/java/com/on/turip/domain/common/paging/Cursor.kt b/android/app/src/main/java/com/on/turip/domain/common/paging/Cursor.kt new file mode 100644 index 000000000..8934499c2 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/domain/common/paging/Cursor.kt @@ -0,0 +1,6 @@ +package com.on.turip.domain.common.paging + +data class Cursor( + val size: Int, + val lastId: Long?, +) diff --git a/android/app/src/main/java/com/on/turip/domain/common/paging/Page.kt b/android/app/src/main/java/com/on/turip/domain/common/paging/Page.kt new file mode 100644 index 000000000..011e74684 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/domain/common/paging/Page.kt @@ -0,0 +1,6 @@ +package com.on.turip.domain.common.paging + +data class Page( + val items: List, + val hasNext: Boolean, +) diff --git a/android/app/src/main/java/com/on/turip/ui/bookmarks/BookmarkContentActivity.kt b/android/app/src/main/java/com/on/turip/ui/bookmarks/BookmarkContentActivity.kt new file mode 100644 index 000000000..79346a6cb --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/bookmarks/BookmarkContentActivity.kt @@ -0,0 +1,73 @@ +package com.on.turip.ui.bookmarks + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import com.on.turip.ui.compose.bookmark.BookmarkContentListScreen +import com.on.turip.ui.compose.designsystem.theme.TuripTheme +import com.on.turip.ui.login.LoginActivity +import com.on.turip.ui.trip.TripDetailActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class BookmarkContentActivity : AppCompatActivity() { + private var hasBookmarkChanges = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + hasBookmarkChanges = + savedInstanceState?.getBoolean(EXTRA_BOOKMARK_CONTENT_HAS_BOOKMARK_CHANGES, false) + ?: false + setContent { + TuripTheme { + BookmarkContentListScreen( + onBack = { + finish() + }, + onNavigateToLogin = { + val intent: Intent = + LoginActivity.newIntent(this).apply { + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + startActivity(intent) + finish() + }, + onNavigateToContent = { contentId: Long -> + val intent: Intent = + TripDetailActivity.newIntent(context = this, contentId = contentId) + startActivity(intent) + }, + onBookmarkChanged = { + hasBookmarkChanges = true + }, + ) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(EXTRA_BOOKMARK_CONTENT_HAS_BOOKMARK_CHANGES, hasBookmarkChanges) + } + + override fun finish() { + val data = + Intent().apply { + putExtra(EXTRA_BOOKMARK_CONTENT_HAS_BOOKMARK_CHANGES, hasBookmarkChanges) + } + setResult(RESULT_OK, data) + super.finish() + } + + companion object { + const val EXTRA_BOOKMARK_CONTENT_HAS_BOOKMARK_CHANGES = + "com.on.turip.ui.bookmarks.BOOKMARK_CHANGES" + + fun newIntent(context: Context): Intent = Intent(context, BookmarkContentActivity::class.java) + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/BookmarkedContentItem.kt b/android/app/src/main/java/com/on/turip/ui/common/component/bookmark/BookmarkContentMetaSection.kt similarity index 52% rename from android/app/src/main/java/com/on/turip/ui/compose/mypage/component/BookmarkedContentItem.kt rename to android/app/src/main/java/com/on/turip/ui/common/component/bookmark/BookmarkContentMetaSection.kt index 53caccb94..751709bac 100644 --- a/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/BookmarkedContentItem.kt +++ b/android/app/src/main/java/com/on/turip/ui/common/component/bookmark/BookmarkContentMetaSection.kt @@ -1,37 +1,25 @@ -package com.on.turip.ui.compose.mypage.component +package com.on.turip.ui.common.component.bookmark import androidx.annotation.DrawableRes -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import coil3.request.ImageRequest -import coil3.request.crossfade import com.on.turip.R import com.on.turip.domain.bookmark.BookmarkContent import com.on.turip.domain.content.Content @@ -39,96 +27,18 @@ import com.on.turip.domain.content.video.VideoData import com.on.turip.domain.creator.Creator import com.on.turip.domain.region.City import com.on.turip.domain.trip.TripDuration -import com.on.turip.ui.common.TuripUrlConverter import com.on.turip.ui.common.mapper.toUiModel import com.on.turip.ui.compose.designsystem.theme.TuripTheme @Composable -fun BookmarkedContentItem( +fun BookmarkContentMetaSection( item: BookmarkContent, - onContentClick: (contentId: Long) -> Unit, onRemoveBookmark: (contentId: Long) -> Unit, modifier: Modifier = Modifier, -) { - Column( - modifier = - modifier - .clip(TuripTheme.shape.container) - .clickable { onContentClick(item.content.id) } - .border(1.dp, TuripTheme.colors.border, TuripTheme.shape.container) - .padding(TuripTheme.spacing.extraSmall), - ) { - ContentThumbnail( - imageUrl = item.content.videoData.url, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(TuripTheme.spacing.medium)) - - Text( - text = item.content.videoData.title, - style = TuripTheme.typography.title2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = TuripTheme.spacing.extraSmall), - ) - - Spacer(modifier = Modifier.height(TuripTheme.spacing.small)) - - ContentInformation( - item = item, - onRemoveBookmark = onRemoveBookmark, - ) - } -} - -@Composable -private fun ContentThumbnail( - imageUrl: String, - modifier: Modifier = Modifier, - contentDescription: String? = null, -) { - val shape = TuripTheme.shape.container - val parsedUrl = TuripUrlConverter.convertVideoThumbnailUrl(imageUrl) - - Box( - modifier = - modifier - .fillMaxWidth() - .aspectRatio(16f / 9f) - .clip(shape) - .border( - width = 1.dp, - color = TuripTheme.colors.gray01, - shape = shape, - ), - ) { - AsyncImage( - model = - ImageRequest - .Builder(LocalContext.current) - .data(parsedUrl) - .crossfade(true) - .build(), - contentDescription = contentDescription, - contentScale = ContentScale.Crop, - modifier = Modifier.matchParentSize(), - error = painterResource(R.drawable.bg_image_placeholder), - ) - } -} - -@Composable -private fun ContentInformation( - item: BookmarkContent, - onRemoveBookmark: (contentId: Long) -> Unit, ) { Row( - modifier = - Modifier - .fillMaxWidth() - .padding(start = TuripTheme.spacing.extraSmall), - verticalAlignment = Alignment.Top, + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, ) { Column( modifier = Modifier.weight(1f), @@ -203,26 +113,26 @@ private fun ContentInfoItem( @Preview(showBackground = true) @Composable -private fun BookmarkedContentItemPreview() { - val content = - BookmarkContent( - content = - Content( - 1L, - Creator(1L, "채널명", ""), - VideoData("콘텐츠 제목이 길면 ...으로 표시되는 것을 확인 ㅇㅇㅇ", "thumbnail", "1박 2일"), - City(""), - true, - ), - tripDuration = TripDuration(1, 2), - tripPlaceCount = 2, - ) +private fun BookmarkContentMetaSectionPreview() { TuripTheme { - BookmarkedContentItem( + val content = + BookmarkContent( + content = + Content( + 1L, + Creator(1L, "채널명", ""), + VideoData("", "", "2026-02-12"), + City(""), + true, + ), + tripDuration = TripDuration(0, 1), + tripPlaceCount = 100, + ) + + BookmarkContentMetaSection( item = content, - onContentClick = {}, onRemoveBookmark = {}, - modifier = Modifier.width(280.dp), + modifier = Modifier.padding(TuripTheme.spacing.large), ) } } diff --git a/android/app/src/main/java/com/on/turip/ui/common/component/bookmark/BookmarkContentTitleRow.kt b/android/app/src/main/java/com/on/turip/ui/common/component/bookmark/BookmarkContentTitleRow.kt new file mode 100644 index 000000000..c46803bb2 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/common/component/bookmark/BookmarkContentTitleRow.kt @@ -0,0 +1,72 @@ +package com.on.turip.ui.common.component.bookmark + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.on.turip.ui.compose.designsystem.theme.TuripTheme + +@Composable +fun BookmarkContentTitleRow( + title: String, + modifier: Modifier = Modifier, + trailing: (@Composable () -> Unit)? = null, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = TuripTheme.typography.title2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + trailing?.invoke() + } +} + +@Preview(showBackground = true, name = "칩 포함") +@Composable +private fun BookmarkContentTitleRowWithTrailingPreview() { + TuripTheme { + BookmarkContentTitleRow( + title = "콘텐츠 제목이 길어질 경우 말줄임 처리 확인용 텍스트입니다", + trailing = { + Text( + text = "테스트", + modifier = + Modifier + .background(TuripTheme.colors.primary) + .padding(TuripTheme.spacing.medium), + ) + }, + modifier = + Modifier + .fillMaxWidth() + .padding(TuripTheme.spacing.large), + ) + } +} + +@Preview(showBackground = true, name = "타이틀만 존재") +@Composable +private fun BookmarkContentTitleRowWithoutTrailingPreview() { + TuripTheme { + BookmarkContentTitleRow( + title = "타이틀만 있는 경우", + trailing = null, + modifier = + Modifier + .fillMaxWidth() + .padding(TuripTheme.spacing.large), + ) + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/common/component/content/ContentThumbnail.kt b/android/app/src/main/java/com/on/turip/ui/common/component/content/ContentThumbnail.kt new file mode 100644 index 000000000..968dd50da --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/common/component/content/ContentThumbnail.kt @@ -0,0 +1,72 @@ +package com.on.turip.ui.common.component.content + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade +import com.on.turip.R +import com.on.turip.ui.common.TuripUrlConverter +import com.on.turip.ui.compose.designsystem.theme.TuripTheme + +@Composable +fun ContentThumbnail( + imageUrl: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { + val shape = TuripTheme.shape.container + val parsedUrl = TuripUrlConverter.convertVideoThumbnailUrl(imageUrl) + + Box( + modifier = + modifier + .fillMaxWidth() + .aspectRatio(16f / 9f) + .clip(shape) + .border( + width = 1.dp, + color = TuripTheme.colors.gray01, + shape = shape, + ), + ) { + AsyncImage( + model = + ImageRequest + .Builder(LocalContext.current) + .data(parsedUrl) + .crossfade(true) + .build(), + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize(), + error = painterResource(R.drawable.bg_image_placeholder), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ContentThumbnailPreview() { + TuripTheme { + ContentThumbnail( + imageUrl = "", + modifier = + Modifier + .background(TuripTheme.colors.primary) + .padding(TuripTheme.spacing.small), + ) + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/common/paging/PagingLoadMode.kt b/android/app/src/main/java/com/on/turip/ui/common/paging/PagingLoadMode.kt new file mode 100644 index 000000000..3690eff72 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/common/paging/PagingLoadMode.kt @@ -0,0 +1,6 @@ +package com.on.turip.ui.common.paging + +enum class PagingLoadMode { + REFRESH, + APPEND, +} diff --git a/android/app/src/main/java/com/on/turip/ui/common/paging/PagingState.kt b/android/app/src/main/java/com/on/turip/ui/common/paging/PagingState.kt new file mode 100644 index 000000000..153ac917b --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/common/paging/PagingState.kt @@ -0,0 +1,13 @@ +package com.on.turip.ui.common.paging + +import androidx.compose.runtime.Immutable +import com.on.turip.ui.common.error.ErrorUiState +import kotlinx.collections.immutable.ImmutableList + +@Immutable +data class PagingState( + val items: ImmutableList, + val hasNext: Boolean, + val isAppending: Boolean, + val errorUiState: ErrorUiState, +) diff --git a/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListScreen.kt b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListScreen.kt new file mode 100644 index 000000000..0bb22dd45 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListScreen.kt @@ -0,0 +1,564 @@ +package com.on.turip.ui.compose.bookmark + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.on.turip.R +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.content.Content +import com.on.turip.domain.content.video.VideoData +import com.on.turip.domain.creator.Creator +import com.on.turip.domain.region.City +import com.on.turip.domain.trip.TripDuration +import com.on.turip.ui.common.error.ErrorUiState +import com.on.turip.ui.common.extensions.showSnackbarWithAction +import com.on.turip.ui.common.paging.PagingState +import com.on.turip.ui.compose.bookmark.component.BookmarkContentListAppBar +import com.on.turip.ui.compose.bookmark.component.BookmarkContentListItem +import com.on.turip.ui.compose.designsystem.component.ErrorScreen +import com.on.turip.ui.compose.designsystem.component.TuripSnackbar +import com.on.turip.ui.compose.designsystem.theme.TuripTheme +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter + +@Composable +fun BookmarkContentListScreen( + onBack: () -> Unit, + onNavigateToLogin: () -> Unit, + onNavigateToContent: (contentId: Long) -> Unit, + onBookmarkChanged: () -> Unit, + viewModel: BookmarkContentListViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + val snackbarHostState = remember { SnackbarHostState() } + val resources = LocalResources.current + + LaunchedEffect(Unit) { + viewModel.uiEffect.collect { uiEffect: BookmarkContentListUiEffect -> + when (uiEffect) { + BookmarkContentListUiEffect.NavigateToLogin -> { + onNavigateToLogin() + } + + BookmarkContentListUiEffect.BookmarkRemovedList -> { + onBookmarkChanged() + } + + BookmarkContentListUiEffect.ShowBookmarkRemoveFailedList -> { + snackbarHostState.showSnackbarWithAction( + message = resources.getString(R.string.my_page_snackbar_bookmark_remove_failed), + actionLabel = resources.getString(R.string.my_page_snackbar_bookmark_remove_failed_action), + onAction = viewModel::refreshBookmarkContents, + ) + } + } + } + } + + Scaffold( + topBar = { BookmarkContentListAppBar(onBackClick = onBack) }, + modifier = + Modifier + .fillMaxSize() + .background(TuripTheme.colors.white) + .systemBarsPadding(), + snackbarHost = { TuripSnackbar(snackbarHostState = snackbarHostState) }, + ) { innerPadding -> + BookmarkContentListContent( + uiState = uiState, + onRetryClick = viewModel::refreshBookmarkContents, + onContentClick = onNavigateToContent, + onBookmarkClick = viewModel::removeBookmark, + onLoadMore = viewModel::loadMoreContents, + modifier = Modifier.padding(innerPadding), + ) + } +} + +@Composable +private fun BookmarkContentListContent( + uiState: BookmarkContentListUiState, + onRetryClick: () -> Unit, + onContentClick: (contentId: Long) -> Unit, + onBookmarkClick: (contentId: Long) -> Unit, + onLoadMore: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxSize()) { + when { + uiState.isLoading -> { + BookmarkContentListLoading() + } + + uiState.errorUiState != ErrorUiState.None -> { + ErrorScreen( + errorUiState = uiState.errorUiState, + onRetryClick = onRetryClick, + modifier = Modifier.fillMaxSize(), + ) + } + + else -> { + if (uiState.isEmpty) { + BookmarkContentListEmpty() + } else { + BookmarkContentList( + uiState = uiState, + onContentClick = onContentClick, + onBookmarkClick = onBookmarkClick, + onLoadMore = onLoadMore, + ) + } + } + } + } +} + +@Composable +private fun BookmarkContentListLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(60.dp), + color = TuripTheme.colors.black, + ) + } +} + +@Composable +private fun BookmarkContentListEmpty(modifier: Modifier = Modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier.fillMaxSize(), + ) { + Spacer(modifier = Modifier.height(TuripTheme.spacing.huge)) + + Image( + painter = painterResource(R.drawable.mascot), + contentDescription = stringResource(R.string.all_mascot_description), + modifier = Modifier.size(70.dp), + ) + + Spacer(modifier = Modifier.height(TuripTheme.spacing.large)) + + Text( + text = stringResource(R.string.my_page_empty_bookmark_content), + style = TuripTheme.typography.title2, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(TuripTheme.spacing.extraHuge)) + } +} + +@Composable +private fun BookmarkContentList( + uiState: BookmarkContentListUiState, + onContentClick: (contentId: Long) -> Unit, + onBookmarkClick: (contentId: Long) -> Unit, + onLoadMore: () -> Unit, + modifier: Modifier = Modifier, +) { + val pagingState: PagingState = uiState.bookmarkContents + val listState = rememberLazyListState() + val threshold = 3 + val shouldLoadMore by remember { + derivedStateOf { + if (!pagingState.hasNext || pagingState.isAppending || pagingState.errorUiState != ErrorUiState.None || + pagingState.items.isEmpty() + ) { + return@derivedStateOf false + } + + val layoutInfo = listState.layoutInfo + val totalCount = layoutInfo.totalItemsCount + val lastVisibleIndex = + layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf false + val lastIndex = totalCount - 1 + + lastVisibleIndex >= lastIndex - threshold + } + } + + LaunchedEffect(Unit) { + snapshotFlow { shouldLoadMore } + .distinctUntilChanged() + .filter { it } + .collect { onLoadMore() } + } + + val totalBookmarkCount = + if (uiState.totalBookmarkCount != null) { + stringResource(R.string.bookmark_content_count, uiState.totalBookmarkCount) + } else { + stringResource(R.string.bookmark_content_count_fail) + } + + Column(modifier = modifier) { + Text( + text = totalBookmarkCount, + textAlign = TextAlign.End, + style = TuripTheme.typography.info2, + color = TuripTheme.colors.gray03, + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = TuripTheme.spacing.medium) + .padding(end = TuripTheme.spacing.large), + ) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 5.dp, + color = TuripTheme.colors.gray01, + ) + + LazyColumn( + state = listState, + contentPadding = PaddingValues(TuripTheme.spacing.medium), + modifier = Modifier.weight(1f), + ) { + itemsIndexed( + items = pagingState.items, + key = { _, item -> item.content.id }, + ) { index, content -> + BookmarkContentListItem( + content = content, + onContentClick = onContentClick, + onRemoveBookmark = onBookmarkClick, + ) + + if (index != pagingState.items.lastIndex) { + HorizontalDivider( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = TuripTheme.spacing.medium), + thickness = 1.dp, + color = TuripTheme.colors.gray01, + ) + } + } + + if (pagingState.isAppending) { + item { + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(TuripTheme.spacing.large), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = TuripTheme.colors.black, + ) + } + } + } else if (pagingState.errorUiState != ErrorUiState.None) { + item { + LoadMoreError(onRetryClick = onLoadMore) + } + } + } + } +} + +@Composable +private fun LoadMoreError( + onRetryClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = TuripTheme.spacing.extraSmall), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.bookmark_content_load_more_fail_title), + style = TuripTheme.typography.info1, + modifier = Modifier.weight(1f), + ) + + TextButton(onClick = onRetryClick) { + Text( + text = stringResource(R.string.retry), + style = TuripTheme.typography.info2, + color = TuripTheme.colors.gray04, + ) + } + } +} + +@Preview(showBackground = true, name = "로딩") +@Composable +private fun BookmarkContentListLoadingPreview() { + TuripTheme { + BookmarkContentListContent( + uiState = BookmarkContentListUiState.Idle, + onRetryClick = {}, + onContentClick = {}, + onBookmarkClick = {}, + onLoadMore = { }, + ) + } +} + +@Preview(showBackground = true, name = "북마크 콘텐츠 비어 있는 경우") +@Composable +private fun BookmarkContentListEmptyPreview() { + TuripTheme { + BookmarkContentListContent( + uiState = + BookmarkContentListUiState( + isLoading = false, + bookmarkContents = + PagingState( + items = persistentListOf(), + hasNext = false, + isAppending = false, + errorUiState = ErrorUiState.None, + ), + totalBookmarkCount = null, + errorUiState = ErrorUiState.None, + ), + onRetryClick = {}, + onContentClick = {}, + onBookmarkClick = {}, + onLoadMore = { }, + ) + } +} + +@Preview(showBackground = true, name = "에러") +@Composable +private fun BookmarkContentListErrorPreview() { + TuripTheme { + BookmarkContentListContent( + uiState = + BookmarkContentListUiState( + isLoading = false, + bookmarkContents = + PagingState( + items = persistentListOf(), + hasNext = false, + isAppending = false, + errorUiState = ErrorUiState.None, + ), + totalBookmarkCount = null, + errorUiState = ErrorUiState.Network, + ), + onRetryClick = {}, + onContentClick = {}, + onBookmarkClick = {}, + onLoadMore = { }, + ) + } +} + +@Preview(showBackground = true, name = "북마크 콘텐츠 수 조회 API만 에러") +@Composable +private fun BookmarkContentListLoadCountErrorPreview() { + val contents = + persistentListOf( + BookmarkContent( + content = + Content( + 1L, + Creator(1L, "채널명", ""), + VideoData("콘텐츠 제목이 길면 ...으로 표시되는 것을 확인 ㅇㅇㅇ", "thumbnail", "2026-01-12"), + City("대구"), + true, + ), + tripDuration = TripDuration(1, 2), + tripPlaceCount = 2, + ), + BookmarkContent( + content = + Content( + 2L, + Creator(1L, "채널명이 길어지는 경우 채널명이 길어지는 경우 채널명이 길어지는 경우 채널명이 길어지는 경우 ", ""), + VideoData("콘텐츠 제목", "thumbnail", "2025-01-12"), + City("대구"), + true, + ), + tripDuration = TripDuration(0, 1), + tripPlaceCount = 2, + ), + ) + TuripTheme { + Column { + BookmarkContentListContent( + uiState = + BookmarkContentListUiState( + isLoading = false, + bookmarkContents = + PagingState( + items = contents, + hasNext = false, + isAppending = false, + errorUiState = ErrorUiState.None, + ), + totalBookmarkCount = null, + errorUiState = ErrorUiState.None, + ), + onRetryClick = {}, + onContentClick = {}, + onBookmarkClick = {}, + onLoadMore = {}, + ) + } + } +} + +@Preview(showBackground = true, name = "더보기 에러") +@Composable +private fun BookmarkContentLoadMoreErrorPreview() { + val contents = + persistentListOf( + BookmarkContent( + content = + Content( + 1L, + Creator(1L, "채널명", ""), + VideoData("콘텐츠 제목", "thumbnail", "2026-01-12"), + City("대구"), + true, + ), + tripDuration = TripDuration(1, 2), + tripPlaceCount = 2, + ), + BookmarkContent( + content = + Content( + 2L, + Creator(2L, "다른 채널", ""), + VideoData("두 번째 콘텐츠", "thumbnail", "2025-01-12"), + City("서울"), + true, + ), + tripDuration = TripDuration(0, 1), + tripPlaceCount = 1, + ), + ) + + TuripTheme { + BookmarkContentListContent( + uiState = + BookmarkContentListUiState( + isLoading = false, + bookmarkContents = + PagingState( + items = contents, + hasNext = true, + isAppending = false, + errorUiState = ErrorUiState.Network, + ), + totalBookmarkCount = 2, + errorUiState = ErrorUiState.None, + ), + onRetryClick = {}, + onContentClick = {}, + onBookmarkClick = {}, + onLoadMore = {}, + ) + } +} + +@Preview(showBackground = true, name = "정상") +@Composable +private fun BookmarkContentListSuccessPreview() { + val contents = + persistentListOf( + BookmarkContent( + content = + Content( + 1L, + Creator(1L, "채널명", ""), + VideoData("콘텐츠 제목이 길면 ...으로 표시되는 것을 확인 ㅇㅇㅇ", "thumbnail", "2026-01-12"), + City("대구"), + true, + ), + tripDuration = TripDuration(1, 2), + tripPlaceCount = 2, + ), + BookmarkContent( + content = + Content( + 2L, + Creator(1L, "채널명이 길어지는 경우 채널명이 길어지는 경우 채널명이 길어지는 경우 채널명이 길어지는 경우 ", ""), + VideoData("콘텐츠 제목", "thumbnail", "2025-01-12"), + City("대구"), + true, + ), + tripDuration = TripDuration(0, 1), + tripPlaceCount = 2, + ), + ) + TuripTheme { + Column { + BookmarkContentListContent( + uiState = + BookmarkContentListUiState( + isLoading = false, + bookmarkContents = + PagingState( + items = contents, + hasNext = false, + isAppending = false, + errorUiState = ErrorUiState.None, + ), + totalBookmarkCount = 10, + errorUiState = ErrorUiState.None, + ), + onRetryClick = {}, + onContentClick = {}, + onBookmarkClick = {}, + onLoadMore = {}, + ) + } + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListUiEffect.kt b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListUiEffect.kt new file mode 100644 index 000000000..4fa665903 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListUiEffect.kt @@ -0,0 +1,9 @@ +package com.on.turip.ui.compose.bookmark + +sealed interface BookmarkContentListUiEffect { + data object NavigateToLogin : BookmarkContentListUiEffect + + data object ShowBookmarkRemoveFailedList : BookmarkContentListUiEffect + + data object BookmarkRemovedList : BookmarkContentListUiEffect +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListUiState.kt b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListUiState.kt new file mode 100644 index 000000000..179dc0146 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListUiState.kt @@ -0,0 +1,34 @@ +package com.on.turip.ui.compose.bookmark + +import androidx.compose.runtime.Immutable +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.ui.common.error.ErrorUiState +import com.on.turip.ui.common.paging.PagingState +import kotlinx.collections.immutable.persistentListOf + +@Immutable +data class BookmarkContentListUiState( + val isLoading: Boolean, + val bookmarkContents: PagingState, + val totalBookmarkCount: Int?, + val errorUiState: ErrorUiState, +) { + val isEmpty: Boolean + get() = !isLoading && bookmarkContents.items.isEmpty() && errorUiState == ErrorUiState.None + + companion object { + val Idle: BookmarkContentListUiState = + BookmarkContentListUiState( + isLoading = true, + bookmarkContents = + PagingState( + items = persistentListOf(), + hasNext = false, + isAppending = false, + errorUiState = ErrorUiState.None, + ), + totalBookmarkCount = null, + errorUiState = ErrorUiState.None, + ) + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListViewModel.kt b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListViewModel.kt new file mode 100644 index 000000000..0df373135 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/BookmarkContentListViewModel.kt @@ -0,0 +1,272 @@ +package com.on.turip.ui.compose.bookmark + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.on.turip.core.result.ErrorType +import com.on.turip.core.result.onFailure +import com.on.turip.core.result.onSuccess +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.bookmark.repository.BookmarkRepository +import com.on.turip.domain.common.paging.Cursor +import com.on.turip.domain.common.paging.Page +import com.on.turip.ui.common.error.ErrorUiState +import com.on.turip.ui.common.error.UiError +import com.on.turip.ui.common.error.toUiError +import com.on.turip.ui.common.paging.PagingLoadMode +import com.on.turip.ui.common.paging.PagingState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class BookmarkContentListViewModel @Inject constructor( + private val bookmarkRepository: BookmarkRepository, +) : ViewModel() { + private val _uiState: MutableStateFlow = + MutableStateFlow(BookmarkContentListUiState.Idle) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _uiEffect: Channel = Channel(Channel.BUFFERED) + val uiEffect: Flow = _uiEffect.receiveAsFlow() + + init { + refreshBookmarkContents() + } + + fun refreshBookmarkContents() { + loadBookmarkContents(PagingLoadMode.REFRESH) + } + + fun loadMoreContents() { + loadBookmarkContents(PagingLoadMode.APPEND) + } + + private fun loadBookmarkContents(loadMode: PagingLoadMode) { + viewModelScope.launch { + if (!prepareLoadingState(loadMode)) return@launch + + // 새로고침할 때만 전체 콘텐츠 수 API 호출 + if (loadMode == PagingLoadMode.REFRESH) launch { loadBookmarkCount() } + + val lastItemId: Long? = + uiState.value.bookmarkContents.items + .lastOrNull() + ?.content + ?.id + + val cursor = Cursor(size = PAGE_SIZE, lastId = lastItemId) + bookmarkRepository + .loadBookmarks(cursor) + .onSuccess { result: Page -> + Timber.d("북마크 화면 조회 성공 mode = $loadMode, cursor = $cursor") + applyBookmarkContents(loadMode, result) + }.onFailure { errorType: ErrorType -> + Timber.e("북마크 화면 에러 loadMode = $loadMode") + val uiError: UiError.Global = errorType.toUiError() as UiError.Global + when (loadMode) { + PagingLoadMode.REFRESH -> applyBookmarkContentsRefreshFailure(uiError) + PagingLoadMode.APPEND -> applyBookmarkContentsAppendFailure(uiError) + } + } + } + } + + private fun prepareLoadingState(loadMode: PagingLoadMode): Boolean { + return when (loadMode) { + PagingLoadMode.REFRESH -> { + _uiState.update { state -> + state.copy( + isLoading = true, + errorUiState = ErrorUiState.None, + bookmarkContents = + state.bookmarkContents.copy( + isAppending = false, + errorUiState = ErrorUiState.None, + ), + ) + } + true + } + + PagingLoadMode.APPEND -> { + val pagingState = uiState.value.bookmarkContents + val canAppend = + pagingState.hasNext && pagingState.items.isNotEmpty() && !pagingState.isAppending + if (!canAppend) return false + + _uiState.update { state -> + state.copy( + isLoading = false, + bookmarkContents = + state.bookmarkContents.copy( + isAppending = true, + errorUiState = ErrorUiState.None, + ), + errorUiState = ErrorUiState.None, + ) + } + true + } + } + } + + private suspend fun loadBookmarkCount() { + bookmarkRepository + .loadBookmarkCount() + .onSuccess { count: Int -> + _uiState.update { state -> state.copy(totalBookmarkCount = count) } + }.onFailure { + _uiState.update { state -> state.copy(totalBookmarkCount = null) } + } + } + + private fun applyBookmarkContents( + loadMode: PagingLoadMode, + result: Page, + ) { + _uiState.update { state -> + val newItems = + when (loadMode) { + PagingLoadMode.REFRESH -> result.items.toImmutableList() + PagingLoadMode.APPEND -> (state.bookmarkContents.items + result.items).toImmutableList() + } + state.copy( + isLoading = false, + bookmarkContents = + PagingState( + items = newItems, + hasNext = result.hasNext, + isAppending = false, + errorUiState = ErrorUiState.None, + ), + errorUiState = ErrorUiState.None, + ) + } + } + + private suspend fun applyBookmarkContentsRefreshFailure(uiError: UiError.Global) { + when (uiError) { + UiError.Global.Network -> { + _uiState.update { + it.copy( + isLoading = false, + errorUiState = ErrorUiState.Network, + ) + } + } + + UiError.Global.Server -> { + _uiState.update { + it.copy( + isLoading = false, + errorUiState = ErrorUiState.Server, + ) + } + } + + UiError.Global.TokenExpired -> { + _uiState.update { it.copy(isLoading = false) } + _uiEffect.send(BookmarkContentListUiEffect.NavigateToLogin) + } + } + } + + private suspend fun applyBookmarkContentsAppendFailure(uiError: UiError.Global) { + when (uiError) { + UiError.Global.Network -> { + _uiState.update { state -> + state.copy( + bookmarkContents = + state.bookmarkContents.copy( + isAppending = false, + errorUiState = ErrorUiState.Network, + ), + ) + } + } + + UiError.Global.Server -> { + _uiState.update { state -> + state.copy( + bookmarkContents = + state.bookmarkContents.copy( + isAppending = false, + errorUiState = ErrorUiState.Server, + ), + ) + } + } + + UiError.Global.TokenExpired -> { + _uiState.update { state -> + state.copy(bookmarkContents = state.bookmarkContents.copy(isAppending = false)) + } + _uiEffect.send(BookmarkContentListUiEffect.NavigateToLogin) + } + } + } + + private val removeBookmarkMutex = Mutex() + + // 삭제 진행 중인 콘텐츠에 대해 중복 API 호출 방지용 + private val removingIds = mutableSetOf() + + // 낙관적 UI + fun removeBookmark(contentId: Long) { + viewModelScope.launch { + val acquired = removeBookmarkMutex.withLock { removingIds.add(contentId) } + // 이미 삭제 진행 중이라면 반환 + if (!acquired) return@launch + + try { + val removed = + removeBookmarkMutex.withLock { + val contents = _uiState.value.bookmarkContents + + // 이미 UI 제거 완료된 상태 (API 호출 완료) + if (contents.items.none { it.content.id == contentId }) return@withLock false + + val updated = + contents.items.filter { it.content.id != contentId }.toImmutableList() + + _uiState.update { state -> + state.copy(bookmarkContents = state.bookmarkContents.copy(items = updated)) + } + + true + } + + if (!removed) return@launch + + bookmarkRepository + .deleteBookmark(contentId) + .onSuccess { + _uiState.update { state -> + state.copy(totalBookmarkCount = state.totalBookmarkCount?.minus(1)) + } + _uiEffect.send(BookmarkContentListUiEffect.BookmarkRemovedList) + }.onFailure { + _uiEffect.send(BookmarkContentListUiEffect.ShowBookmarkRemoveFailedList) + } + } finally { + // 중복 API 호출 방지 리소스 정리 + removeBookmarkMutex.withLock { removingIds.remove(contentId) } + } + } + } + + companion object { + private const val PAGE_SIZE = 20 + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/bookmark/component/BookmarkContentListAppBar.kt b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/component/BookmarkContentListAppBar.kt new file mode 100644 index 000000000..ccde624b6 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/component/BookmarkContentListAppBar.kt @@ -0,0 +1,55 @@ +package com.on.turip.ui.compose.bookmark.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.on.turip.R +import com.on.turip.ui.compose.designsystem.component.TuripAppBar +import com.on.turip.ui.compose.designsystem.theme.TuripTheme + +@Composable +fun BookmarkContentListAppBar( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TuripAppBar( + start = { + IconButton( + onClick = onBackClick, + modifier = Modifier.size(36.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.all_back_description), + ) + } + }, + center = { + Text( + text = stringResource(R.string.bookmark_content_title), + style = TuripTheme.typography.title1, + ) + }, + modifier = modifier, + ) +} + +@Preview(showBackground = true) +@Composable +private fun BookmarkContentListAppBarPreview() { + TuripTheme { + BookmarkContentListAppBar( + onBackClick = {}, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/bookmark/component/BookmarkContentListItem.kt b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/component/BookmarkContentListItem.kt new file mode 100644 index 000000000..e936334f1 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/compose/bookmark/component/BookmarkContentListItem.kt @@ -0,0 +1,123 @@ +package com.on.turip.ui.compose.bookmark.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.content.Content +import com.on.turip.domain.content.video.VideoData +import com.on.turip.domain.creator.Creator +import com.on.turip.domain.region.City +import com.on.turip.domain.trip.TripDuration +import com.on.turip.ui.common.component.bookmark.BookmarkContentMetaSection +import com.on.turip.ui.common.component.bookmark.BookmarkContentTitleRow +import com.on.turip.ui.common.component.content.ContentThumbnail +import com.on.turip.ui.compose.designsystem.theme.TuripTheme + +@Composable +fun BookmarkContentListItem( + content: BookmarkContent, + onContentClick: (contentId: Long) -> Unit, + onRemoveBookmark: (contentId: Long) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxWidth() + .clip(TuripTheme.shape.container) + .clickable { onContentClick(content.content.id) } + .background(TuripTheme.colors.white) + .padding(TuripTheme.spacing.extraSmall), + ) { + ContentThumbnail( + imageUrl = content.content.videoData.url, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(TuripTheme.spacing.medium)) + + BookmarkContentTitleRow( + title = content.content.videoData.title, + trailing = { + BookmarkRegionChip( + regionName = content.content.city.name, + modifier = Modifier.padding(horizontal = TuripTheme.spacing.small), + ) + }, + ) + + Spacer(modifier = Modifier.height(TuripTheme.spacing.small)) + + BookmarkContentMetaSection( + item = content, + onRemoveBookmark = onRemoveBookmark, + ) + } +} + +@Composable +private fun BookmarkRegionChip( + regionName: String, + modifier: Modifier = Modifier, +) { + Box( + modifier = + modifier + .wrapContentSize() + .background( + color = TuripTheme.colors.chipBackground, + shape = TuripTheme.shape.chip, + ) + .padding( + horizontal = TuripTheme.spacing.medium, + vertical = TuripTheme.spacing.extraSmall, + ), + ) { + Text( + text = regionName, + style = TuripTheme.typography.info1, + color = TuripTheme.colors.black, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun BookmarkContentListItemPreview() { + TuripTheme { + val content = + BookmarkContent( + content = + Content( + 1L, + Creator(1L, "채널명", ""), + VideoData("콘텐츠 제목이 길면 ...으로 표시되는 것을 확인 ㅇㅇㅇ", "thumbnail", "2026-02-12"), + City("대구"), + true, + ), + tripDuration = TripDuration(1, 2), + tripPlaceCount = 2, + ) + BookmarkContentListItem( + content = content, + onContentClick = {}, + onRemoveBookmark = {}, + modifier = + Modifier + .padding(TuripTheme.spacing.large) + .background(TuripTheme.colors.white), + ) + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt b/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt index e44b68c79..26e5203d8 100644 --- a/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt +++ b/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt @@ -19,20 +19,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.on.turip.R +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.content.Content +import com.on.turip.domain.content.video.VideoData +import com.on.turip.domain.creator.Creator +import com.on.turip.domain.region.City +import com.on.turip.domain.trip.TripDuration import com.on.turip.ui.common.error.toUiModel import com.on.turip.ui.common.extensions.dismissAndExecute import com.on.turip.ui.common.extensions.showSnackbarWithAction import com.on.turip.ui.compose.designsystem.component.TuripDialog import com.on.turip.ui.compose.designsystem.component.TuripSnackbar import com.on.turip.ui.compose.designsystem.theme.TuripTheme -import com.on.turip.ui.compose.mypage.component.BookmarkedContentSection import com.on.turip.ui.compose.mypage.component.MyPageAppBar +import com.on.turip.ui.compose.mypage.component.MyPageBookmarkContentSection import com.on.turip.ui.compose.mypage.component.MyPageSettingsSection import com.on.turip.ui.compose.mypage.component.ProfileSection import com.on.turip.ui.compose.mypage.model.InquiryMail +import com.on.turip.ui.compose.mypage.model.ProfileModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Composable fun MyPageScreen( @@ -200,7 +211,7 @@ private fun MyPageScreenContent( ) } item { - BookmarkedContentSection( + MyPageBookmarkContentSection( state = uiState.bookmarkContentState, onViewAllContentClick = onNavigateToAllBookmarkContents, onContentClick = onNavigateToContent, @@ -223,12 +234,83 @@ private fun MyPageScreenContent( } } +private class MyPageUiStatePreviewProvider : PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + // 둘 다 성공 + MyPageUiState.Idle.copy( + profileState = + MyPageSectionState.Success( + ProfileModel( + 1L, + "닉네임은 최대 2줄 까지 가능하도록 보여지고 있어요 닉네임은 최대 2줄 까지 가능하도록 보여지고 있어요", + null, + ), + ), + bookmarkContentState = MyPageSectionState.Success(previewBookmarkContents()), + ), + // 프로필 & 북마크 에러 + MyPageUiState.Idle.copy( + profileState = MyPageSectionState.Error, + bookmarkContentState = MyPageSectionState.Error, + ), + // 북마크 로딩 + MyPageUiState.Idle.copy( + bookmarkContentState = MyPageSectionState.Loading, + ), + // 로그아웃 다이얼로그 + MyPageUiState.Idle.copy(dialogState = MyPageDialogState.LogoutRequired), + // 회원 탈퇴 다이얼로그 + MyPageUiState.Idle.copy(dialogState = MyPageDialogState.ConfirmWithdraw), + ) +} + +private fun previewBookmarkContents(): ImmutableList = + persistentListOf( + BookmarkContent( + content = + Content( + id = 1L, + creator = Creator(1L, "채널명", ""), + videoData = + VideoData( + title = "콘텐츠 제목이 길면 말줄임 처리되는지 확인합니다", + url = "thumbnail", + uploadedDate = "2026-02-23", + ), + city = City("대구"), + isBookmarked = true, + ), + tripDuration = TripDuration(1, 2), + tripPlaceCount = 3, + ), + BookmarkContent( + content = + Content( + id = 2L, + creator = Creator(2L, "긴 채널명 긴 채널명 긴 채널명", ""), + videoData = + VideoData( + title = "짧은 제목", + url = "thumbnail", + uploadedDate = "2026-02-10", + ), + city = City("제주"), + isBookmarked = true, + ), + tripDuration = TripDuration(0, 1), + tripPlaceCount = 5, + ), + ) + @Preview(showBackground = true) @Composable -private fun MyPageScreenPreview() { +private fun MyPageScreenPreview( + @PreviewParameter(MyPageUiStatePreviewProvider::class) uiState: MyPageUiState, +) { TuripTheme { MyPageScreenContent( - uiState = MyPageUiState.Idle, + uiState = uiState, snackbarHostState = remember { SnackbarHostState() }, onNavigateToAllBookmarkContents = {}, onNavigateToContent = {}, diff --git a/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageViewModel.kt b/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageViewModel.kt index d4956d745..c686c5826 100644 --- a/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageViewModel.kt +++ b/android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageViewModel.kt @@ -5,10 +5,12 @@ import androidx.lifecycle.viewModelScope import com.on.turip.core.result.ErrorType import com.on.turip.core.result.onFailure import com.on.turip.core.result.onSuccess -import com.on.turip.domain.accounts.Account -import com.on.turip.domain.accounts.AccountRepository -import com.on.turip.domain.bookmark.PagedBookmarkContents +import com.on.turip.domain.account.Account +import com.on.turip.domain.account.AccountRepository +import com.on.turip.domain.bookmark.BookmarkContent import com.on.turip.domain.bookmark.repository.BookmarkRepository +import com.on.turip.domain.common.paging.Cursor +import com.on.turip.domain.common.paging.Page import com.on.turip.domain.login.MemberRepository import com.on.turip.domain.setting.PrivacyPolicy import com.on.turip.domain.userstorage.repository.UserStorageRepository @@ -72,12 +74,13 @@ class MyPageViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(bookmarkContentState = MyPageSectionState.Loading) } + val cursor = Cursor(size = 10, lastId = null) bookmarkRepository - .loadBookmarks(10, 0L) - .onSuccess { result: PagedBookmarkContents -> + .loadBookmarks(cursor) + .onSuccess { result: Page -> Timber.d("마이페이지 북마크 목록 조회 성공") _uiState.update { - it.copy(bookmarkContentState = MyPageSectionState.Success(result.bookmarkContents.toImmutableList())) + it.copy(bookmarkContentState = MyPageSectionState.Success(result.items.toImmutableList())) } }.onFailure { Timber.e("마이페이지 북마크 목록 조회 에러 발생") diff --git a/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageBookmarkContentItem.kt b/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageBookmarkContentItem.kt new file mode 100644 index 000000000..9d663d3e3 --- /dev/null +++ b/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageBookmarkContentItem.kt @@ -0,0 +1,89 @@ +package com.on.turip.ui.compose.mypage.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.on.turip.domain.bookmark.BookmarkContent +import com.on.turip.domain.content.Content +import com.on.turip.domain.content.video.VideoData +import com.on.turip.domain.creator.Creator +import com.on.turip.domain.region.City +import com.on.turip.domain.trip.TripDuration +import com.on.turip.ui.common.component.bookmark.BookmarkContentMetaSection +import com.on.turip.ui.common.component.bookmark.BookmarkContentTitleRow +import com.on.turip.ui.common.component.content.ContentThumbnail +import com.on.turip.ui.compose.designsystem.theme.TuripTheme + +@Composable +fun MyPageBookmarkContentItem( + item: BookmarkContent, + onContentClick: (contentId: Long) -> Unit, + onRemoveBookmark: (contentId: Long) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .border(1.dp, TuripTheme.colors.border, TuripTheme.shape.container) + .clip(TuripTheme.shape.container) + .clickable { onContentClick(item.content.id) } + .background(TuripTheme.colors.white) + .padding(TuripTheme.spacing.extraSmall), + ) { + ContentThumbnail( + imageUrl = item.content.videoData.url, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(TuripTheme.spacing.medium)) + + BookmarkContentTitleRow(title = item.content.videoData.title) + + Spacer(modifier = Modifier.height(TuripTheme.spacing.small)) + + BookmarkContentMetaSection( + item = item, + onRemoveBookmark = onRemoveBookmark, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun MyPageBookmarkContentItemPreview() { + val content = + BookmarkContent( + content = + Content( + 1L, + Creator(1L, "채널명", ""), + VideoData("콘텐츠 제목이 길면 ...으로 표시되는 것을 확인 ㅇㅇㅇ", "thumbnail", "2026-02-23"), + City(""), + true, + ), + tripDuration = TripDuration(1, 2), + tripPlaceCount = 2, + ) + TuripTheme { + MyPageBookmarkContentItem( + item = content, + onContentClick = {}, + onRemoveBookmark = {}, + modifier = + Modifier + .width(280.dp) + .padding(TuripTheme.spacing.large), + ) + } +} diff --git a/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/BookmarkedContentSection.kt b/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageBookmarkContentSection.kt similarity index 90% rename from android/app/src/main/java/com/on/turip/ui/compose/mypage/component/BookmarkedContentSection.kt rename to android/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageBookmarkContentSection.kt index 2a336fce2..117f12d5c 100644 --- a/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/BookmarkedContentSection.kt +++ b/android/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageBookmarkContentSection.kt @@ -44,7 +44,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Composable -fun BookmarkedContentSection( +fun MyPageBookmarkContentSection( state: MyPageSectionState>, onViewAllContentClick: () -> Unit, onContentClick: (contentId: Long) -> Unit, @@ -81,7 +81,7 @@ fun BookmarkedContentSection( modifier = Modifier.padding(start = TuripTheme.spacing.large), ) { items(items = state.data, key = { it.content.id }) { - BookmarkedContentItem( + MyPageBookmarkContentItem( item = it, onContentClick = onContentClick, onRemoveBookmark = onRemoveBookmark, @@ -228,24 +228,24 @@ private fun EmptyBookmarkedContent() { @Preview(showBackground = true, name = "북마크한 컨텐츠 없음 ") @Composable -private fun BookmarkedContentSectionEmptyPreview() { +private fun MyPageBookmarkContentSectionEmptyPreview() { TuripTheme { - BookmarkedContentSection( + MyPageBookmarkContentSection( state = MyPageSectionState.Success(persistentListOf()), onViewAllContentClick = {}, onContentClick = {}, onRemoveBookmark = {}, onRetry = {}, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.padding(TuripTheme.spacing.large), ) } } @Preview(showBackground = true, name = "컨텐츠 존재") @Composable -private fun BookmarkedContentSectionWithItemsPreview() { +private fun MyPageBookmarkContentSectionWithItemsPreview() { TuripTheme { - BookmarkedContentSection( + MyPageBookmarkContentSection( state = MyPageSectionState.Success( persistentListOf( @@ -257,7 +257,7 @@ private fun BookmarkedContentSectionWithItemsPreview() { VideoData( "콘텐츠 제목이 길면 ...으로 표시되는 것을 확인 ㅇㅇㅇ", "thumbnail", - "1박 2일", + "2026-01-02", ), City(""), true, @@ -271,37 +271,46 @@ private fun BookmarkedContentSectionWithItemsPreview() { onContentClick = {}, onRemoveBookmark = {}, onRetry = {}, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .padding(TuripTheme.spacing.large), ) } } @Preview(showBackground = true, name = "로딩 중") @Composable -private fun BookmarkedContentSectionLoadingPreview() { +private fun MyPageBookmarkContentSectionLoadingPreview() { TuripTheme { - BookmarkedContentSection( + MyPageBookmarkContentSection( state = MyPageSectionState.Loading, onViewAllContentClick = {}, onContentClick = {}, onRemoveBookmark = {}, onRetry = {}, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .padding(TuripTheme.spacing.large), ) } } @Preview(showBackground = true, name = "에러") @Composable -private fun BookmarkedContentSectionErrorPreview() { +private fun MyPageBookmarkContentSectionErrorPreview() { TuripTheme { - BookmarkedContentSection( + MyPageBookmarkContentSection( state = MyPageSectionState.Error, onViewAllContentClick = {}, onContentClick = {}, onRemoveBookmark = {}, onRetry = {}, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .padding(TuripTheme.spacing.large), ) } } diff --git a/android/app/src/main/java/com/on/turip/ui/compose/mypage/util/MyPageMapper.kt b/android/app/src/main/java/com/on/turip/ui/compose/mypage/util/MyPageMapper.kt index 9bfa76204..74c8e5085 100644 --- a/android/app/src/main/java/com/on/turip/ui/compose/mypage/util/MyPageMapper.kt +++ b/android/app/src/main/java/com/on/turip/ui/compose/mypage/util/MyPageMapper.kt @@ -1,6 +1,6 @@ package com.on.turip.ui.compose.mypage.util -import com.on.turip.domain.accounts.Account +import com.on.turip.domain.account.Account import com.on.turip.ui.compose.mypage.model.ProfileModel fun Account.toUiModel(): ProfileModel = diff --git a/android/app/src/main/java/com/on/turip/ui/main/MainActivity.kt b/android/app/src/main/java/com/on/turip/ui/main/MainActivity.kt index 8ee860e24..6fbc7cd97 100644 --- a/android/app/src/main/java/com/on/turip/ui/main/MainActivity.kt +++ b/android/app/src/main/java/com/on/turip/ui/main/MainActivity.kt @@ -16,9 +16,8 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : BaseActivity() { - override val binding: ActivityMainBinding by lazy { - ActivityMainBinding.inflate(layoutInflater) - } + override val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) } + private var backPressedTime: Long = 0L override fun onCreate(savedInstanceState: Bundle?) { diff --git a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentFragment.kt b/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentFragment.kt index 352cccfff..93e2144dd 100644 --- a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentFragment.kt +++ b/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentFragment.kt @@ -1,54 +1,15 @@ package com.on.turip.ui.main.bookmarks -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels -import com.google.android.material.snackbar.Snackbar -import com.on.turip.R import com.on.turip.databinding.FragmentFavoriteContentBinding -import com.on.turip.domain.bookmark.BookmarkContent -import com.on.turip.ui.common.ItemDividerDecoration import com.on.turip.ui.common.base.BaseFragment -import com.on.turip.ui.common.error.ErrorUiModel -import com.on.turip.ui.common.error.ErrorUiState -import com.on.turip.ui.common.error.toUiModel -import com.on.turip.ui.common.extensions.collectOnStarted -import com.on.turip.ui.login.LoginActivity -import com.on.turip.ui.trip.TripDetailActivity import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber @AndroidEntryPoint class BookmarkContentFragment : BaseFragment() { - private val viewModel: BookmarkContentViewModel by viewModels() - - private val favoriteContentAdapter: FavoriteContentAdapter by lazy { - FavoriteContentAdapter( - object : FavoriteContentViewHolder.FavoriteContentListener { - override fun onFavoriteClick( - contentId: Long, - isFavorite: Boolean, - ) { - Timber.d("컨텐츠 목록의 북마크 버튼을 클릭(contentId=$contentId)\n업데이트 된 북마크 상태 =${!isFavorite}") - viewModel.updateBookmark(contentId, isFavorite) - } - - override fun onFavoriteItemClick(contentId: Long) { - Timber.d("컨텐츠 목록의 아이템 클릭(contentId=$contentId)") - val intent: Intent = - TripDetailActivity.newIntent( - context = requireContext(), - contentId = contentId, - ) - startActivity(intent) - } - }, - ) - } - override fun inflateBinding( inflater: LayoutInflater, container: ViewGroup?, @@ -59,73 +20,7 @@ class BookmarkContentFragment : BaseFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - setupAdapters() - setupObservers() - } - - private fun setupAdapters() { - binding.rvFavoriteContentContents.apply { - adapter = favoriteContentAdapter - addItemDecoration( - ItemDividerDecoration( - height = 1, - color = requireContext().getColor(R.color.gray_100_f0f0ee), - ), - ) - } - } - - private fun setupObservers() { - collectOnStarted(viewModel.uiState) { uiState: BookmarkContentUiState -> - if (uiState.isLoading) showLoading() - when { - uiState.errorUiState != ErrorUiState.None -> showErrorView(uiState.errorUiState) - uiState.isEmpty -> showEmptyView() - else -> showContents(uiState.bookmarkContents) - } - } - - collectOnStarted(viewModel.uiEffect) { uiEffect: BookmarkContentUiEffect -> - when (uiEffect) { - BookmarkContentUiEffect.NavigateToLogin -> { - navigateToLoginScreen() - } - - is BookmarkContentUiEffect.ShowError -> { - val uiModel: ErrorUiModel = - uiEffect.errorUiState.toUiModel() ?: return@collectOnStarted - view?.let { view: View -> - Snackbar - .make(view, uiModel.titleRes, Snackbar.LENGTH_INDEFINITE) - .apply { - setAction(uiModel.retryTextRes) { - viewModel.handleErrorRetryRequest(uiEffect.action) - } - }.show() - } - } - } - } - } - - private fun showLoading() { - binding.pbFavoriteContentLoading.visibility = View.VISIBLE - binding.clFavoriteContentEmpty.visibility = View.GONE - binding.clFavoriteContentNotEmpty.visibility = View.GONE - binding.customErrorView.visibility = View.GONE - } - - private fun showErrorView(errorUiState: ErrorUiState) { - binding.customErrorView.visibility = View.VISIBLE - binding.pbFavoriteContentLoading.visibility = View.GONE - binding.clFavoriteContentEmpty.visibility = View.GONE - binding.clFavoriteContentNotEmpty.visibility = View.GONE - - binding.customErrorView.apply { - visibility = View.VISIBLE - showErrorView(errorUiState) - setOnRetryClickListener { viewModel.loadBookmarkContents() } - } + showEmptyView() } private fun showEmptyView() { @@ -135,36 +30,6 @@ class BookmarkContentFragment : BaseFragment() { binding.clFavoriteContentNotEmpty.visibility = View.GONE } - private fun showContents(bookmarkContents: List) { - binding.customErrorView.visibility = View.GONE - binding.pbFavoriteContentLoading.visibility = View.GONE - - binding.clFavoriteContentNotEmpty.visibility = View.VISIBLE - binding.clFavoriteContentEmpty.visibility = View.GONE - favoriteContentAdapter.submitList(bookmarkContents) - } - - private fun navigateToLoginScreen() { - val intent: Intent = - LoginActivity - .newIntent(requireActivity()) - .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } - startActivity(intent) - requireActivity().finish() - } - - override fun onResume() { - super.onResume() - viewModel.loadBookmarkContents() - } - - override fun onHiddenChanged(hidden: Boolean) { - super.onHiddenChanged(hidden) - if (!hidden) { - viewModel.loadBookmarkContents() - } - } - companion object { fun instance(): BookmarkContentFragment = BookmarkContentFragment() } diff --git a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentUiEffect.kt b/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentUiEffect.kt deleted file mode 100644 index f1ea4e027..000000000 --- a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentUiEffect.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.on.turip.ui.main.bookmarks - -import com.on.turip.ui.common.error.ErrorUiState - -sealed interface BookmarkContentUiEffect { - data object NavigateToLogin : BookmarkContentUiEffect - - data class ShowError( - val errorUiState: ErrorUiState, - val action: BookmarkContentRetryAction, - ) : BookmarkContentUiEffect -} - -sealed interface BookmarkContentRetryAction { - data class UpdateBookmark( - val contentId: Long, - val isBookmarked: Boolean, - ) : BookmarkContentRetryAction -} diff --git a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentUiState.kt b/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentUiState.kt deleted file mode 100644 index bfcc23228..000000000 --- a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentUiState.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.on.turip.ui.main.bookmarks - -import com.on.turip.domain.bookmark.BookmarkContent -import com.on.turip.ui.common.error.ErrorUiState - -data class BookmarkContentUiState( - val isLoading: Boolean, - val bookmarkContents: List, - val errorUiState: ErrorUiState, -) { - val isEmpty: Boolean - get() = bookmarkContents.isEmpty() && this != Idle - - companion object { - val Idle: BookmarkContentUiState = - BookmarkContentUiState( - isLoading = true, - bookmarkContents = emptyList(), - errorUiState = ErrorUiState.None, - ) - } -} diff --git a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentViewModel.kt b/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentViewModel.kt deleted file mode 100644 index 018ff3d71..000000000 --- a/android/app/src/main/java/com/on/turip/ui/main/bookmarks/BookmarkContentViewModel.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.on.turip.ui.main.bookmarks - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.on.turip.core.result.ErrorType -import com.on.turip.core.result.onFailure -import com.on.turip.core.result.onSuccess -import com.on.turip.domain.bookmark.PagedBookmarkContents -import com.on.turip.domain.bookmark.repository.BookmarkRepository -import com.on.turip.domain.bookmark.usecase.UpdateBookmarkUseCase -import com.on.turip.ui.common.error.ErrorUiState -import com.on.turip.ui.common.error.UiError -import com.on.turip.ui.common.error.toUiError -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@HiltViewModel -class BookmarkContentViewModel @Inject constructor( - private val bookmarkRepository: BookmarkRepository, - private val updateBookmarkUseCase: UpdateBookmarkUseCase, -) : ViewModel() { - private val _uiState: MutableStateFlow = - MutableStateFlow(BookmarkContentUiState.Idle) - val uiState: StateFlow = _uiState.asStateFlow() - - private val _uiEffect: Channel = Channel(Channel.BUFFERED) - val uiEffect: Flow = _uiEffect.receiveAsFlow() - - init { - loadBookmarkContents() - } - - fun loadBookmarkContents() { - viewModelScope.launch { - _uiState.update { it.copy(isLoading = true) } - - bookmarkRepository - .loadBookmarks(10, 0L) - .onSuccess { result: PagedBookmarkContents -> - Timber.d("북마크 목록 조회 성공") - _uiState.update { - it.copy( - isLoading = false, - bookmarkContents = result.bookmarkContents, - errorUiState = ErrorUiState.None, - ) - } - }.onFailure { errorType: ErrorType -> - Timber.e("북마크 목록 조회 에러 발생") - val uiError: UiError = errorType.toUiError() - if (uiError is UiError.Global) { - when (uiError) { - UiError.Global.Network -> { - _uiState.update { - it.copy(isLoading = false, errorUiState = ErrorUiState.Network) - } - } - - UiError.Global.Server -> { - _uiState.update { - it.copy(isLoading = false, errorUiState = ErrorUiState.Server) - } - } - - UiError.Global.TokenExpired -> { - _uiState.update { it.copy(isLoading = false) } - _uiEffect.send(BookmarkContentUiEffect.NavigateToLogin) - } - } - } - } - } - } - - fun updateBookmark( - contentId: Long, - isBookmarked: Boolean, - ) { - val updatedBookmark: Boolean = !isBookmarked - - viewModelScope.launch { - updateBookmarkUseCase(updatedBookmark, contentId) - .onSuccess { - Timber.d("북마크 목록 화면, 북마크 클릭(contentId=$contentId, updatedBookmark = $updatedBookmark") - - _uiState.update { state: BookmarkContentUiState -> - state.copy( - isLoading = false, - bookmarkContents = state.bookmarkContents.filter { it.content.id != contentId }, - errorUiState = ErrorUiState.None, - ) - } - }.onFailure { errorType: ErrorType -> - Timber.e("북마크 목록 화면, 북마크 클릭 실패(contentId=$contentId, originBookmark = $isBookmarked)") - _uiState.update { it.copy(isLoading = false) } - val uiError: UiError = errorType.toUiError() - if (uiError is UiError.Global) { - when (uiError) { - UiError.Global.Network -> { - _uiEffect.send( - BookmarkContentUiEffect.ShowError( - errorUiState = ErrorUiState.Network, - action = - BookmarkContentRetryAction.UpdateBookmark( - contentId = contentId, - isBookmarked = isBookmarked, - ), - ), - ) - } - - UiError.Global.Server -> { - _uiEffect.send( - BookmarkContentUiEffect.ShowError( - errorUiState = ErrorUiState.Server, - action = - BookmarkContentRetryAction.UpdateBookmark( - contentId = contentId, - isBookmarked = isBookmarked, - ), - ), - ) - } - - UiError.Global.TokenExpired -> { - _uiEffect.send(BookmarkContentUiEffect.NavigateToLogin) - } - } - } - } - } - } - - fun handleErrorRetryRequest(action: BookmarkContentRetryAction) { - when (action) { - is BookmarkContentRetryAction.UpdateBookmark -> { - updateBookmark(action.contentId, action.isBookmarked) - } - } - } -} diff --git a/android/app/src/main/java/com/on/turip/ui/mypage/MyPageActivity.kt b/android/app/src/main/java/com/on/turip/ui/mypage/MyPageActivity.kt index 209a23d73..5a30fc429 100644 --- a/android/app/src/main/java/com/on/turip/ui/mypage/MyPageActivity.kt +++ b/android/app/src/main/java/com/on/turip/ui/mypage/MyPageActivity.kt @@ -6,12 +6,17 @@ import android.net.Uri import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri import com.on.turip.R +import com.on.turip.ui.bookmarks.BookmarkContentActivity +import com.on.turip.ui.bookmarks.BookmarkContentActivity.Companion.EXTRA_BOOKMARK_CONTENT_HAS_BOOKMARK_CHANGES import com.on.turip.ui.common.extensions.safeStartActivityWithToast import com.on.turip.ui.compose.designsystem.theme.TuripTheme import com.on.turip.ui.compose.mypage.MyPageScreen +import com.on.turip.ui.compose.mypage.MyPageViewModel import com.on.turip.ui.compose.mypage.model.InquiryMail import com.on.turip.ui.login.LoginActivity import com.on.turip.ui.trip.TripDetailActivity @@ -20,6 +25,8 @@ import timber.log.Timber @AndroidEntryPoint class MyPageActivity : AppCompatActivity() { + private val viewModel: MyPageViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -27,8 +34,10 @@ class MyPageActivity : AppCompatActivity() { setContent { TuripTheme { MyPageScreen( - // 화면 구현 필요 - onNavigateToAllBookmarkContents = {}, + onNavigateToAllBookmarkContents = { + val intent = BookmarkContentActivity.newIntent(this) + bookmarkContentLauncher.launch(intent) + }, onNavigateToContent = { contentId: Long -> Timber.d("마이페이지 북마크 콘텐츠 클릭(contentId=$contentId)") val intent: Intent = @@ -40,9 +49,11 @@ class MyPageActivity : AppCompatActivity() { "mailto:${InquiryMail.RECIPIENT}?subject=${Uri.encode(InquiryMail.TITLE)}&body=${ Uri.encode(mail.content) }".toUri() - val intent: Intent = Intent(Intent.ACTION_SENDTO).apply { data = uri } - startActivity(intent) + safeStartActivityWithToast( + intent = intent, + errorToastMessage = getString(R.string.all_snackbar_not_found_inquiry_url), + ) }, onNavigateToPrivacyPolicy = { url: String -> val intent = Intent(Intent.ACTION_VIEW, url.toUri()) @@ -60,11 +71,25 @@ class MyPageActivity : AppCompatActivity() { startActivity(intent) finish() }, + viewModel = viewModel, ) } } } + private val bookmarkContentLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != RESULT_OK) return@registerForActivityResult + + val changed = + result.data?.getBooleanExtra(EXTRA_BOOKMARK_CONTENT_HAS_BOOKMARK_CHANGES, false) + ?: false + + if (changed) { + viewModel.loadBookmarkContents(isRetry = false) + } + } + companion object { fun newIntent(context: Context): Intent = Intent(context, MyPageActivity::class.java) } diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 4c7c5a4c7..c063d5cdb 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -21,6 +21,7 @@ 지도 앱으로 이동에 문제가 발생 했어요 개인 정보 처리 방침 이동에 문제가 발생 했어요 영상 앱으로 이동에 문제가 발생 했어요 + 문의하기 이동에 문제가 발생 했어요 공유 하기 마스코트 @@ -54,7 +55,7 @@ 개인 정보 처리 방침 로그인 로그아웃 - 회원 탈퇴 + 회원탈퇴 정말 로그아웃 하시겠습니까? 로그아웃 취소 @@ -70,8 +71,12 @@ 저장한 콘텐츠들을 불러오지 못했어요. + 저장한 콘텐츠 + 콘텐츠 %d개 컨텐츠를 저장 해보세요! 원하는 동선이 담긴\n여행 일정을 저장해 보세요. + 데이터를 불러올 수 없습니다. 잠시 후 재시도 해주세요 + 콘텐츠 - 튜립에 장소를 추가 해보세요!