diff --git a/app/src/main/java/com/eighteen/eighteenandroid/common/Constants.kt b/app/src/main/java/com/eighteen/eighteenandroid/common/Constants.kt index 30d3f73f..57cfc478 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/common/Constants.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/common/Constants.kt @@ -1,3 +1,5 @@ package com.eighteen.eighteenandroid.common -const val MEDIA_COUNT = 2 \ No newline at end of file +const val MEDIA_COUNT = 2 +const val USER_TYPE_SIGNIN = "user" +const val USER_TYPE_GUEST = "guest" \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/common/ResourceProvider.kt b/app/src/main/java/com/eighteen/eighteenandroid/common/ResourceProvider.kt new file mode 100644 index 00000000..f298f2c2 --- /dev/null +++ b/app/src/main/java/com/eighteen/eighteenandroid/common/ResourceProvider.kt @@ -0,0 +1,18 @@ +package com.eighteen.eighteenandroid.common + +import android.content.Context +import androidx.annotation.StringRes +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class ResourceProvider @Inject constructor( + @ApplicationContext private val context: Context +) { + fun getString(@StringRes resId: Int): String { + return context.getString(resId) + } + + fun getString(@StringRes resId: Int, vararg args: Any): String { + return context.getString(resId, *args) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/common/enums/Tag.kt b/app/src/main/java/com/eighteen/eighteenandroid/common/enums/Tag.kt index ed52eb25..8f1138a9 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/common/enums/Tag.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/common/enums/Tag.kt @@ -3,7 +3,7 @@ package com.eighteen.eighteenandroid.common.enums enum class Tag(val strValue: String) { ALL("전체"), BEAUTY("뷰티"), - EXERCISE("운동"), + SPORT("운동"), STUDY("공부"), ART("예술"), GAME("게임"), diff --git a/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/response/UserResponse.kt b/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/response/UserResponse.kt index 8c14ec2d..335835dc 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/response/UserResponse.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/response/UserResponse.kt @@ -1,10 +1,20 @@ package com.eighteen.eighteenandroid.data.datasource.remote.response +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class UserResponse( - val userImage: String, - val userId: String, - val name: String, - val age: String, - val userSchoolName: String, - val tag: String + val users: List, + val totalPage: Int +) + +data class UserDto( + val id: Int, + val profileImage: String?, + val uniqueId: String, + val nickName: String, + val birthDay: String, + val location: String, + val schoolName: String, + val likeStatus: Boolean ) diff --git a/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/service/UserService.kt b/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/service/UserService.kt index 22d5089a..28282246 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/service/UserService.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/data/datasource/remote/service/UserService.kt @@ -3,6 +3,7 @@ package com.eighteen.eighteenandroid.data.datasource.remote.service import com.eighteen.eighteenandroid.data.datasource.remote.request.SignUpRequest import com.eighteen.eighteenandroid.data.datasource.remote.response.ApiResult import com.eighteen.eighteenandroid.data.datasource.remote.response.ProfileDetailResponse +import com.eighteen.eighteenandroid.data.datasource.remote.response.UserDto import com.eighteen.eighteenandroid.data.datasource.remote.response.UserResponse import retrofit2.Response import retrofit2.http.Body @@ -31,6 +32,18 @@ interface UserService { @POST("/v1/api/user/sign-in") suspend fun postLogin(@Query("phoneNumber") phoneNumber: String): Response> + @GET("/v1/api/{userType}/find-all/{category}") + suspend fun getCategoryUser(@Path("userType") userType: String, @Path("category") category: String, @Query("page") page: Int, @Query("size") size: Int = 10): Response> + + @POST("/v1/api/user/like") + suspend fun postLikeUser(@Query("likedId") likedId: Int): Response> + + @POST("/v1/api/user/like-cancel") + suspend fun postLikeCancelUser(@Query("likedId") likedId: Int): Response> + + @GET("/v1/api/teen/{userType}/famous/{category}") + suspend fun getPopularUser(@Path("userType") userType: String, @Path("category") category: String): Response>> + @DELETE("/v1/api/user/sign-out") suspend fun signOut(): Response> diff --git a/app/src/main/java/com/eighteen/eighteenandroid/data/mapper/UserMapper.kt b/app/src/main/java/com/eighteen/eighteenandroid/data/mapper/UserMapper.kt index cf95a518..f634dab8 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/data/mapper/UserMapper.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/data/mapper/UserMapper.kt @@ -1,18 +1,49 @@ package com.eighteen.eighteenandroid.data.mapper +import com.eighteen.eighteenandroid.data.datasource.remote.response.UserDto import com.eighteen.eighteenandroid.data.datasource.remote.response.UserResponse import com.eighteen.eighteenandroid.domain.model.User +import com.eighteen.eighteenandroid.domain.model.UserUseCaseModel +import java.time.LocalDate +import java.time.Period +import java.time.format.DateTimeFormatter object UserMapper { - fun asUserCaseModel(userResponse: UserResponse) = - userResponse.run { - User( - userImage = userImage, - userId = userId, - userName = name, - userAge = age, - userSchoolName = userSchoolName, - tag = tag - ) + fun UserDto.toUser(): User { + fun calculateAge(birthDay: String): Int { + // 날짜 포맷터 생성 + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + + try { + // 문자열을 LocalDate 객체로 변환 + val birthDate = LocalDate.parse(birthDay, formatter) + + // 현재 날짜 가져오기 + val currentDate = LocalDate.now() + + // Period를 사용하여 두 날짜 사이의 기간 계산 + val period = Period.between(birthDate, currentDate) + + // 만 나이 계산 + return period.years + } catch (e: Exception) { + throw IllegalArgumentException("올바른 날짜 형식이 아닙니다. 'yyyy-MM-dd' 형식으로 입력해주세요.") + } } + + return User( + userId = id, + userImage = profileImage?: "", + uniqueId = uniqueId, + userName = nickName, + userAge = calculateAge(birthDay).toString(), + userSchoolName = schoolName, + likeStatus = likeStatus + ) + } + + fun UserResponse.toUserUseCaseModel() = UserUseCaseModel( + users = users.map { it.toUser() }, + totalPageCount = totalPage + ) } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/eighteen/eighteenandroid/data/repository/UserRepositoryImpl.kt index d19083bb..7e376e90 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/data/repository/UserRepositoryImpl.kt @@ -12,6 +12,8 @@ import com.eighteen.eighteenandroid.data.datasource.remote.request.SchoolRequest import com.eighteen.eighteenandroid.data.datasource.remote.request.SignUpRequest import com.eighteen.eighteenandroid.data.datasource.remote.service.UserService import com.eighteen.eighteenandroid.data.mapper.ApiException +import com.eighteen.eighteenandroid.data.mapper.UserMapper.toUser +import com.eighteen.eighteenandroid.data.mapper.UserMapper.toUserUseCaseModel import com.eighteen.eighteenandroid.data.mapper.mapper import com.eighteen.eighteenandroid.domain.model.AuthToken import com.eighteen.eighteenandroid.domain.model.Mbti @@ -23,6 +25,7 @@ import com.eighteen.eighteenandroid.domain.model.School import com.eighteen.eighteenandroid.domain.model.SignUpInfo import com.eighteen.eighteenandroid.domain.model.SnsLink import com.eighteen.eighteenandroid.domain.model.User +import com.eighteen.eighteenandroid.domain.model.UserUseCaseModel import com.eighteen.eighteenandroid.domain.repository.UserRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -32,40 +35,32 @@ class UserRepositoryImpl @Inject constructor( private val userService: UserService, private val preferenceDatastore: DataStore ) : UserRepository { - override suspend fun fetchUserData(): Result> = + override suspend fun getAnotherUser(userType: String, category: String, page: Int): Result = runCatching { -// userService.getUserInfo().mapper { -// it.map { userResponse -> -// UserMapper.asUserCaseModel(userResponse) -// } -// } - - listOf( - User( - userImage = "https://image.blip.kr/v1/file/021ec61ff1c9936943383b84236a0e69", - userId = "1", - userName = "김 에스더", - userAge = "16", - userSchoolName = "서울 중학교", - tag = "운동" - ), - User( - userImage = "https://cdn.newsculture.press/news/photo/202308/529742_657577_5726.jpg", - userId = "2", - userName = "김 에스더", - userAge = "16", - userSchoolName = "부천 중학교", - tag = "스터디" - ), - User( - userImage = "https://mblogthumb-phinf.pstatic.net/MjAyMTEwMzFfMTY1/MDAxNjM1NjUzMTI2NjI3.xXYQteLLoWLKcR9YnXS0Hk_y-DInauMzF25g7FxlcScg.2Y-neBBMVoP2IhcwzX2Zy2HB2d8EnM_cY76FVLuk_1Yg.JPEG.ssun2415/IMG_4148.jpg?type=w800", - userId = "3", - userName = "김 에스더", - userAge = "16", - userSchoolName = "인천 중학교", - tag = "프로젝트" - ) - ) + userService.getCategoryUser(userType, category, page).mapper { + it.data?.toUserUseCaseModel() ?: UserUseCaseModel(emptyList(), 0) + } + } + + override suspend fun postLikeUser(likedId: Int): Result = + runCatching { + userService.postLikeUser(likedId).mapper { + it.data ?: throw ApiException.Unknown + } + } + + override suspend fun postLikeCancelUser(likedId: Int): Result = + runCatching { + userService.postLikeCancelUser(likedId).mapper { + it.data ?: throw ApiException.Unknown + } + } + + override suspend fun getPopularUser(userType: String, category: String): Result> = + runCatching { + userService.getPopularUser(userType, category).mapper { + it.data?.map{ userDto -> userDto.toUser() } ?: throw ApiException.Unknown + } } override suspend fun fetchUserDetailInfo(id: String): Result = runCatching { diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/di/UserUseCaseModule.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/di/UserUseCaseModule.kt index 604134a5..0e501e71 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/domain/di/UserUseCaseModule.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/di/UserUseCaseModule.kt @@ -4,12 +4,14 @@ import com.eighteen.eighteenandroid.domain.repository.MyPageRepository import com.eighteen.eighteenandroid.domain.repository.UserRepository import com.eighteen.eighteenandroid.domain.usecase.CheckIdDuplicationUseCase import com.eighteen.eighteenandroid.domain.usecase.DeleteUserUseCase +import com.eighteen.eighteenandroid.domain.usecase.GetAnotherUserUseCase import com.eighteen.eighteenandroid.domain.usecase.GetMyProfileUseCase +import com.eighteen.eighteenandroid.domain.usecase.GetPopularUserUseCase import com.eighteen.eighteenandroid.domain.usecase.GetUserDetailInfoUseCase import com.eighteen.eighteenandroid.domain.usecase.LoginUseCase import com.eighteen.eighteenandroid.domain.usecase.SignOutUseCase import com.eighteen.eighteenandroid.domain.usecase.SignUpUseCase -import com.eighteen.eighteenandroid.domain.usecase.UserUseCase +import com.eighteen.eighteenandroid.domain.usecase.UserLikeUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,10 +21,6 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object UserUseCaseModule { - @Provides - @Singleton - fun provideUserUseCase(repository: UserRepository): UserUseCase = - UserUseCase(repository = repository) @Provides @Singleton @@ -55,4 +53,16 @@ object UserUseCaseModule { @Singleton fun provideDeleteUserUseCase(repository: UserRepository) = DeleteUserUseCase(repository = repository) + + @Provides + @Singleton + fun provideGetAnotherUserUseCase(repository: UserRepository) = GetAnotherUserUseCase(repository = repository) + + @Provides + @Singleton + fun provideGetPopularUserUseCase(repository: UserRepository) = GetPopularUserUseCase(repository = repository) + + @Provides + @Singleton + fun provideUserLikeUseCase(repository: UserRepository) = UserLikeUseCase(repository = repository) } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/model/MainItem.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/model/MainItem.kt index 21cc5f00..ad74eb50 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/domain/model/MainItem.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/model/MainItem.kt @@ -33,4 +33,7 @@ sealed class MainItem{ data class UserView( val user: User ): MainItem() + + // LoadingView + object LoadingView: MainItem() } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/model/User.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/model/User.kt index e6bdfa0c..e589e440 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/domain/model/User.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/model/User.kt @@ -1,10 +1,11 @@ package com.eighteen.eighteenandroid.domain.model data class User( + val userId: Int, val userImage: String, - val userId: String, + val uniqueId: String, val userName: String, val userAge: String, val userSchoolName: String, - val tag: String + var likeStatus: Boolean ) diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/model/UserUseCaseModel.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/model/UserUseCaseModel.kt new file mode 100644 index 00000000..488d20c5 --- /dev/null +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/model/UserUseCaseModel.kt @@ -0,0 +1,6 @@ +package com.eighteen.eighteenandroid.domain.model + +data class UserUseCaseModel( + val users: List, + val totalPageCount: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/repository/UserRepository.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/repository/UserRepository.kt index fa2e04f2..dbcd83c8 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/repository/UserRepository.kt @@ -4,10 +4,10 @@ import com.eighteen.eighteenandroid.domain.model.AuthToken import com.eighteen.eighteenandroid.domain.model.Profile import com.eighteen.eighteenandroid.domain.model.SignUpInfo import com.eighteen.eighteenandroid.domain.model.User +import com.eighteen.eighteenandroid.domain.model.UserUseCaseModel import kotlinx.coroutines.flow.Flow interface UserRepository { - suspend fun fetchUserData(): Result> suspend fun fetchUserDetailInfo(id: String): Result suspend fun postSignUp(signUpInfo: SignUpInfo): Result suspend fun checkIdDuplication(uniqueId: String): Result @@ -17,4 +17,8 @@ interface UserRepository { suspend fun signOut(): Result suspend fun deleteUser(): Result suspend fun deleteAuthToken() + suspend fun getAnotherUser(userType: String, category: String, page: Int): Result + suspend fun getPopularUser(userType: String, category: String): Result> + suspend fun postLikeUser(likedId: Int): Result + suspend fun postLikeCancelUser(likedId: Int): Result } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/GetAnotherUserUseCase.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/GetAnotherUserUseCase.kt new file mode 100644 index 00000000..d7a0acd5 --- /dev/null +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/GetAnotherUserUseCase.kt @@ -0,0 +1,8 @@ +package com.eighteen.eighteenandroid.domain.usecase + +import com.eighteen.eighteenandroid.domain.repository.UserRepository +import javax.inject.Inject + +class GetAnotherUserUseCase @Inject constructor(private val repository: UserRepository) { + suspend operator fun invoke(category: String, userType: String, page: Int) = repository.getAnotherUser(userType, category, page) +} \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/GetPopularUserUseCase.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/GetPopularUserUseCase.kt new file mode 100644 index 00000000..4e661174 --- /dev/null +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/GetPopularUserUseCase.kt @@ -0,0 +1,8 @@ +package com.eighteen.eighteenandroid.domain.usecase + +import com.eighteen.eighteenandroid.domain.repository.UserRepository +import javax.inject.Inject + +class GetPopularUserUseCase @Inject constructor(private val repository: UserRepository) { + suspend operator fun invoke(category: String, userType: String) = repository.getPopularUser(userType, category) +} \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/UserLikeUseCase.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/UserLikeUseCase.kt new file mode 100644 index 00000000..fcf22686 --- /dev/null +++ b/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/UserLikeUseCase.kt @@ -0,0 +1,13 @@ +package com.eighteen.eighteenandroid.domain.usecase + +import com.eighteen.eighteenandroid.domain.repository.UserRepository +import javax.inject.Inject + +class UserLikeUseCase @Inject constructor(private val repository: UserRepository) { + suspend operator fun invoke(isLike: Boolean, likedId: Int) = + if(isLike) { + repository.postLikeUser(likedId) + } else { + repository.postLikeCancelUser(likedId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/UserUseCase.kt b/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/UserUseCase.kt deleted file mode 100644 index ce3f11a3..00000000 --- a/app/src/main/java/com/eighteen/eighteenandroid/domain/usecase/UserUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.eighteen.eighteenandroid.domain.usecase - -import com.eighteen.eighteenandroid.domain.model.User -import com.eighteen.eighteenandroid.domain.repository.UserRepository -import javax.inject.Inject - -class UserUseCase @Inject constructor(private val repository: UserRepository) { - suspend fun invoke(): Result> = - repository.fetchUserData() -} \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/MyViewModel.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/MyViewModel.kt index 145d4479..79325b0b 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/MyViewModel.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/MyViewModel.kt @@ -1,5 +1,7 @@ package com.eighteen.eighteenandroid.presentation +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.eighteen.eighteenandroid.domain.model.AuthToken @@ -36,10 +38,12 @@ class MyViewModel @Inject constructor( private val _myProfileStateFlow = MutableStateFlow>(ModelState.Empty()) val myProfileStateFlow = _myProfileStateFlow.asStateFlow() - private val _editProfileEventStateFlow = - MutableStateFlow>>(ModelState.Empty()) + private val _editProfileEventStateFlow = MutableStateFlow>>(ModelState.Empty()) val editProfileEventStateFlow = _editProfileEventStateFlow.asStateFlow() + private val _userSignEventLiveData = MutableLiveData>() + val userSignEventLiveData: LiveData> = _userSignEventLiveData + private var myProfileJob: Job? = null private var editMyProfileJob: Job? = null @@ -52,11 +56,12 @@ class MyViewModel @Inject constructor( requestMyProfile() } - fun completeLogin(authToken: AuthToken) { + fun completeLogin(authToken: AuthToken?) { if (myProfileJob?.isCompleted == false) return myProfileJob = viewModelScope.launch { _myProfileStateFlow.value = ModelState.Loading() - saveAuthTokenUseCase.invoke(authToken) + authToken?.let { saveAuthTokenUseCase.invoke(it) } + _userSignEventLiveData.value = Event(authToken) getMyProfileUseCase.invoke().onSuccess { _myProfileStateFlow.value = ModelState.Success(it) }.onFailure { diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpFragment.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpFragment.kt index f8047174..25510ec0 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpFragment.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpFragment.kt @@ -184,6 +184,7 @@ class SignUpFragment : BaseFragment(FragmentSignUpBinding private fun initSignUpResultStateFlow() { collectInLifecycle(signUpViewModel.signUpResultStateFlow) { when (it) { + is ModelState.Loading -> {} is ModelState.Success -> { it.data?.let { authToken -> myViewModel.completeLogin(authToken = authToken) @@ -191,7 +192,7 @@ class SignUpFragment : BaseFragment(FragmentSignUpBinding findNavController().navigate(R.id.action_fragmentSignUp_to_fragmentSignUpCompleted) } else -> { - //do nothing + myViewModel.completeLogin(null) } } } diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModel.kt index 1647ff09..eba21b13 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModel.kt @@ -69,8 +69,8 @@ class SignUpViewModel @Inject constructor( override val pageClearEvent: StateFlow> = _pageClearEventStateFlow.asStateFlow() - private val _requestLoginEventLiveData = MutableLiveData>() - val requestLoginEventLiveData: LiveData> = _requestLoginEventLiveData + private val _requestLoginEventLiveData = MutableLiveData>() + val requestLoginEventLiveData: LiveData> = _requestLoginEventLiveData override var phoneNumber: String = "" override var id: String = "" @@ -191,7 +191,7 @@ class SignUpViewModel @Inject constructor( } } - override fun requestLogin(authToken: AuthToken) { + override fun requestLogin(authToken: AuthToken?) { viewModelScope.launch { _requestLoginEventLiveData.value = Event(authToken) } diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModelContentInterface.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModelContentInterface.kt index 6f5a802d..4c14ef75 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModelContentInterface.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/SignUpViewModelContentInterface.kt @@ -34,6 +34,6 @@ interface SignUpViewModelContentInterface { fun setPageClearEvent(page: SignUpPage) fun removeMedia(position: Int) fun setMainMedia(position: Int) - fun requestLogin(authToken: AuthToken) + fun requestLogin(authToken: AuthToken?) fun sendSignUpStatusEvent(event: SignUpStatusEvent) } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/enterauthcode/SignUpEnterAuthCodeFragment.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/enterauthcode/SignUpEnterAuthCodeFragment.kt index 360ef6a0..5353d996 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/enterauthcode/SignUpEnterAuthCodeFragment.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/enterauthcode/SignUpEnterAuthCodeFragment.kt @@ -193,6 +193,8 @@ class SignUpEnterAuthCodeFragment : if (data is ConfirmResultModel.LoginSuccess) { signUpViewModelContentInterface.requestLogin(authToken = data.authToken) } else { + // 로그인 실패 + signUpViewModelContentInterface.requestLogin(authToken = null) signUpViewModelContentInterface.sendSignUpStatusEvent(event = SignUpStatusEvent.ERROR_DIALOG) } } diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/selecttag/SignUpSelectTagViewModel.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/selecttag/SignUpSelectTagViewModel.kt index 3abf2d68..13de2367 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/selecttag/SignUpSelectTagViewModel.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/auth/signup/selecttag/SignUpSelectTagViewModel.kt @@ -22,6 +22,6 @@ class SignUpSelectTagViewModel : ViewModel() { } companion object { - private val tags = listOf(Tag.BEAUTY, Tag.EXERCISE, Tag.STUDY, Tag.ART, Tag.GAME, Tag.ETC) + private val tags = listOf(Tag.BEAUTY, Tag.SPORT, Tag.STUDY, Tag.ART, Tag.GAME, Tag.ETC) } } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/dialog/ReportDialogFragment.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/dialog/ReportDialogFragment.kt index b0cc6e55..cf2bd362 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/dialog/ReportDialogFragment.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/dialog/ReportDialogFragment.kt @@ -52,7 +52,7 @@ class ReportDialogFragment : fun newInstance(user: User): ReportDialogFragment { val bundle = Bundle().apply { - putString(KEY_USER_ID, user.userId) + putString(KEY_USER_ID, user.uniqueId) putString(KEY_USER_NAME, user.userName) } return ReportDialogFragment().apply { arguments = bundle } diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainFragment.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainFragment.kt index 76437875..be1c5f16 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainFragment.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainFragment.kt @@ -4,7 +4,9 @@ import android.content.Context import android.util.DisplayMetrics import android.view.View import android.view.animation.AlphaAnimation +import android.widget.ImageButton import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -14,12 +16,14 @@ import androidx.recyclerview.widget.RecyclerView import com.eighteen.eighteenandroid.R import com.eighteen.eighteenandroid.common.enums.Tag import com.eighteen.eighteenandroid.databinding.FragmentMainBinding -import com.eighteen.eighteenandroid.domain.model.AboutTeen -import com.eighteen.eighteenandroid.domain.model.MainItem import com.eighteen.eighteenandroid.domain.model.Tournament import com.eighteen.eighteenandroid.domain.model.User import com.eighteen.eighteenandroid.presentation.BaseFragment +import com.eighteen.eighteenandroid.presentation.MyViewModel +import com.eighteen.eighteenandroid.presentation.common.ModelState +import com.eighteen.eighteenandroid.presentation.common.collectInLifecycle import com.eighteen.eighteenandroid.presentation.common.createChip +import com.eighteen.eighteenandroid.presentation.common.dp2Px import com.eighteen.eighteenandroid.presentation.common.findViewHolderOrNull import com.eighteen.eighteenandroid.presentation.common.setTagStyle import com.eighteen.eighteenandroid.presentation.common.showDialogFragment @@ -43,13 +47,12 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class MainFragment : BaseFragment(FragmentMainBinding::inflate) { private val viewModel by viewModels() + private val myViewModel by activityViewModels() - private var selectedChip: Chip? = null private lateinit var mainAdapter: MainAdapter - private var userList = listOf() - private var aboutTeenList = listOf() - private var tournamentList = listOf() +// private var aboutTeenList = listOf() +// private var tournamentList = listOf() private lateinit var mainAdapterListener: MainAdapterListener @@ -59,6 +62,14 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl private var autoScrollJob: Job? = null private var isAutoScrolling = false + private var isLoading = false + private var isRequestNextPage = false + + // 현재 카테고리 + private var selectedChip: Chip? = null // 칩 버튼 View + private var category: Tag = Tag.ALL // 카테고리 정보 + + private lateinit var _pageNumList: List override fun initView() { initChipGroup() @@ -101,36 +112,49 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl private fun initMainAdapter() { initMainAdapterListener() - mainAdapter = - MainAdapter(context = requireContext(), listener = mainAdapterListener).apply { - stateRestorationPolicy = - RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY // 현재 스크롤 위치 저장 + mainAdapter = MainAdapter(context = requireContext(), listener = mainAdapterListener).apply { + stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY // 현재 스크롤 위치 저장 } bind { with(rvMain) { adapter = mainAdapter + itemAnimator = null // Item Notify animation 제거 addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition() + val itemTotalCount = recyclerView.adapter!!.itemCount-1 + + // 스크롤이 끝에 도달했는지 확인 + if (::_pageNumList.isInitialized && !recyclerView.canScrollVertically(1) && lastVisibleItemPosition == itemTotalCount) { + if( _pageNumList.isNotEmpty()) { + if(!isLoading) { + isRequestNextPage = true + val page = _pageNumList.random() + viewModel.removePage(page) + viewModel.requestNextPage(category, page) + } + } + } + // 스크롤이 위로 되면 (dy < 0) 버튼 숨기기, 아래로 스크롤 시( dy > 0 ) 버튼 보여주기 if (dy > 0 && btnScrollTop.visibility == View.GONE) { btnScrollTop.startAnimation(fadeIn) btnScrollTop.visibility = View.VISIBLE } - } - - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) if (!canScrollVertically(-1)) { isTop = true } else { isTop = false } + } + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) val layoutManager = (layoutManager as? LinearLayoutManager) // Log.i("MainScrollStateChanged", "findLastVisible = ${layoutManager?.findLastVisibleItemPosition().toString()}") @@ -147,7 +171,6 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl when (newState) { RecyclerView.SCROLL_STATE_IDLE -> { - viewModel.pageScrollPosition = getCenterItemPosition(recyclerView) if (isTop && btnScrollTop.isVisible) { btnScrollTop.startAnimation(fadeOut) @@ -168,27 +191,14 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl // Top 버튼 btnScrollTop.throttleClick(viewLifecycleOwner.lifecycleScope) { - appbarLayout.setExpanded(true, true) - rvMain.smoothScrollToPosition(0) + moveToTop() } } + } -// livedata.observe() { - // page1 -> 10개 - - // current - // 전체 리스트를 주진 않고 - -// current + 10 -// } - - // 마지막 아이템을 만나면 - - // 데이터 함수 호출 - - // 라이브데이터 값 바꾸고 - - // 뷰 갱신 + private fun moveToTop() { + binding.appbarLayout.setExpanded(true, true) + binding.rvMain.scrollToPosition(0) } private fun initMainAdapterListener() { @@ -204,9 +214,14 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl /** * 유저 좋아요 클릭 */ - override fun onUserLikeClicks(user: User) { + override fun onUserLikeClicks(likeBtn: ImageButton, user: User) { stopAutoScroll() - // TODO. User Like API 호출 + val likeStatus = likeBtn.isSelected + + requestWithRequiredLogin { + // User Like API 호출 + viewModel.requestUserLike(likeStatus.not(), likedId = user.userId) + } } /** @@ -242,7 +257,7 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl when(title) { "Teen" -> bottomNavigationView.selectedItemId = R.id.teenMainFragment "채팅" -> bottomNavigationView.selectedItemId = R.id.fragmentChat - "토너먼트" -> {} + "토너먼트" -> bottomNavigationView.selectedItemId = R.id.fragmentRanking "나만의 Teen" -> bottomNavigationView.selectedItemId = R.id.fragmentMyProfile } } @@ -268,8 +283,7 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl * 이전에 보던 인기 Teen 유저로 이동 */ override fun scrollToPreviousUser() { - val popularUserListViewHolder = - binding.rvMain.findViewHolderOrNull() + val popularUserListViewHolder = binding.rvMain.findViewHolderOrNull() val rvPopularUserList = popularUserListViewHolder?.binding?.rvMainTeenPopularList val layoutManager = rvPopularUserList?.layoutManager as? LinearLayoutManager @@ -292,10 +306,6 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl viewModel.popularUserPosition = position } - override fun saveScrollPosition(position: Int) { - viewModel.pageScrollPosition = position - } - override fun startAutoScroll() { isAutoScrolling = true autoScrollJob = viewLifecycleOwner.lifecycleScope.launch { @@ -345,142 +355,87 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl private fun initMain() { initMainAdapter() - initData() - initUserListObserver() + initMainItemObserver() } - private fun initUserListObserver() { -// viewModel.mainItems.observe(viewLifecycleOwner) { -// mainAdapter.updateView(it) -// } + private fun initMainItemObserver() { + // 남은 페이지 목록 + viewModel.pageNumList.observe(viewLifecycleOwner) { + _pageNumList = it + } - viewModel.userData.observe(viewLifecycleOwner) { - userList = it - updateMain() + myViewModel.userSignEventLiveData.observe(viewLifecycleOwner) { + getUserData(category) } - } - private fun updateMain() { - mainAdapter.updateView( - listOf( - MainItem.HeaderView(resources.getString(R.string.main_today_teen)), - MainItem.UserListView( - userList - ), // User List - MainItem.DividerView, - MainItem.HeaderView(getString(R.string.main_about_teen)), - MainItem.AboutTeenListView( -// aboutTeenList - listOf( - AboutTeen("Teen", "친구들의 프로필을 투표해보세요!"), - AboutTeen("토너먼트", "투표 결과를 한 눈에 볼 수 있어요!"), - AboutTeen("채팅", "채팅을 통해 친구들과 소통해보세요!"), - AboutTeen("나만의 Teen", "나만의 프로필을 등록해보세요!") - ) - ), // About Teen List - MainItem.DividerView, - MainItem.HeaderWithMoreView(getString(R.string.main_tournament_in_progress)), - MainItem.TournamentListView( -// tournamentList - listOf( - Tournament.Exercise, - Tournament.Study - ) - ), // Tournament List - MainItem.DividerView, - MainItem.HeaderView(getString(R.string.main_another_teen)), - MainItem.UserView( - User( - userImage = "https://image.blip.kr/v1/file/021ec61ff1c9936943383b84236a0e69", - userId = "1", - userName = "김 에스더", - userAge = "16", - userSchoolName = "서울 중학교", - tag = "운동" - ) - ), - MainItem.UserView( - User( - userImage = "https://cdn.newsculture.press/news/photo/202308/529742_657577_5726.jpg", - userId = "2", - userName = "김 에스더", - userAge = "16", - userSchoolName = "서울 중학교", - tag = "운동" - ) - ), - MainItem.UserView( - User( - userImage = "https://mblogthumb-phinf.pstatic.net/MjAyMTEwMzFfMTY1/MDAxNjM1NjUzMTI2NjI3.xXYQteLLoWLKcR9YnXS0Hk_y-DInauMzF25g7FxlcScg.2Y-neBBMVoP2IhcwzX2Zy2HB2d8EnM_cY76FVLuk_1Yg.JPEG.ssun2415/IMG_4148.jpg?type=w800", - userId = "3", - userName = "김 에스더", - userAge = "16", - userSchoolName = "서울 중학교", - tag = "운동" - ) - ) - ) - ) - } + collectInLifecycle(viewModel.mainItemStateFlow) { it -> + when(it) { + is ModelState.Loading -> { + isLoading = true + mainAdapter.addLoadingView { lastPosition -> + binding.rvMain.scrollToPosition(lastPosition) + } + } + is ModelState.Success -> { + isLoading = false + if(isRequestNextPage.not()) { + moveToTop() + } + + mainAdapter.removeLoadingView() + it.data?.let { mainItems -> + mainAdapter.updateView(mainItems) + } + } + else ->{ + // Error + mainAdapter.removeLoadingView() + } + } + } + + collectInLifecycle(viewModel.userLikeStateFlow) { + when(it) { + is ModelState.Loading -> { + + } + is ModelState.Success -> { + it.data?.let { userId -> + mainAdapter.updateUserLikeStatus(binding.rvMain, userId, isLike = true) + } + } + + is ModelState.Error -> { + + } + + else -> { + //do nothing + } + } + } + + collectInLifecycle(viewModel.userLikeCancelStateFlow) { + when(it) { + is ModelState.Loading -> { + + } + is ModelState.Success -> { + it.data?.let { userId -> + mainAdapter.updateUserLikeStatus(binding.rvMain, userId, isLike = false) + } + } + + is ModelState.Error -> { + + } + + else -> { + //do nothing + } + } + } - private fun initData() { - mainAdapter.updateView( - listOf( - MainItem.UserListView( - emptyList() - ), // User List - MainItem.DividerView, - MainItem.HeaderView(getString(R.string.main_about_teen)), - MainItem.AboutTeenListView( - listOf( - AboutTeen("Teen", "친구들의 프로필을 투표해보세요!"), - AboutTeen("토너먼트", "투표 결과를 한 눈에 볼 수 있어요!"), - AboutTeen("채팅", "채팅을 통해 친구들과 소통해보세요!"), - AboutTeen("나만의 Teen", "나만의 프로필을 등록해보세요!") - ) - ), // About Teen List - MainItem.DividerView, - MainItem.HeaderWithMoreView(getString(R.string.main_tournament_in_progress)), - MainItem.TournamentListView( - listOf( - Tournament.Exercise, - Tournament.Study - ) - ), // Tournament List - MainItem.DividerView, - MainItem.HeaderView(getString(R.string.main_another_teen)), - MainItem.UserView( - User( - userImage = "https://image.blip.kr/v1/file/021ec61ff1c9936943383b84236a0e69", - userId = "1", - userName = "김 에스더", - userAge = "16", - userSchoolName = "서울 중학교", - tag = "운동" - ) - ), - MainItem.UserView( - User( - userImage = "https://cdn.newsculture.press/news/photo/202308/529742_657577_5726.jpg", - userId = "2", - userName = "김 에스더", - userAge = "16", - userSchoolName = "서울 중학교", - tag = "운동" - ) - ), - MainItem.UserView( - User( - userImage = "https://mblogthumb-phinf.pstatic.net/MjAyMTEwMzFfMTY1/MDAxNjM1NjUzMTI2NjI3.xXYQteLLoWLKcR9YnXS0Hk_y-DInauMzF25g7FxlcScg.2Y-neBBMVoP2IhcwzX2Zy2HB2d8EnM_cY76FVLuk_1Yg.JPEG.ssun2415/IMG_4148.jpg?type=w800", - userId = "3", - userName = "김 에스더", - userAge = "16", - userSchoolName = "서울 중학교", - tag = "운동" - ) - ) - ) - ) } private fun initChipGroup() { @@ -489,11 +444,22 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl if (tag == Tag.ALL) { // 화면 최초 진입 시 전체 태그가 클릭된 상태여야함 chip.setTagStyle(isBlackBackground = true) selectedChip = chip + category = tag } chip.setOnClickListener { _ -> + if(isLoading) { + // TODO. 토스트 ? + return@setOnClickListener + } + + isRequestNextPage = false + selectedChip?.setTagStyle(isBlackBackground = false) chip.setTagStyle(isBlackBackground = true) selectedChip = chip + category = tag // 현재 카테고리 값 저장 + + getUserData(tag) // 현재 카테고리에 맞는 데이터 가져오기 } bind { chipGroup.addView(chip) @@ -501,27 +467,8 @@ class MainFragment : BaseFragment(FragmentMainBinding::infl } } - override fun onResume() { - super.onResume() - lifecycleScope.launch { - delay(300) -// bind { -// // 마지막 스크롤 상태로 돌아오기 -// if (viewModel.pageScrollPosition != -1) { -// -// rvMain.scrollToPosition(viewModel.pageScrollPosition) -// -// // LayoutManager에서 해당 위치의 아이템을 중앙에 위치시키도록 오프셋 조정 -// rvMain.post { -// val layoutManager = rvMain.layoutManager as LinearLayoutManager -// val viewAtPosition = layoutManager.findViewByPosition(viewModel.pageScrollPosition) -// if (viewAtPosition != null) { -// val offset = (rvMain.height - viewAtPosition.height) / 2 -// layoutManager.scrollToPositionWithOffset(viewModel.pageScrollPosition, offset) -// } -// } -// } -// } - } + private fun getUserData(tag: Tag) { + mainAdapter.removeAllViews() + viewModel.initMain(tag) } } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainViewModel.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainViewModel.kt index 8924b1ab..a702a3e2 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainViewModel.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/MainViewModel.kt @@ -1,76 +1,245 @@ package com.eighteen.eighteenandroid.presentation.home -import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.eighteen.eighteenandroid.R +import com.eighteen.eighteenandroid.common.ResourceProvider +import com.eighteen.eighteenandroid.common.USER_TYPE_SIGNIN +import com.eighteen.eighteenandroid.common.USER_TYPE_GUEST +import com.eighteen.eighteenandroid.common.enums.Tag +import com.eighteen.eighteenandroid.data.mapper.ApiException +import com.eighteen.eighteenandroid.domain.model.AboutTeen import com.eighteen.eighteenandroid.domain.model.MainItem -import com.eighteen.eighteenandroid.domain.model.User -import com.eighteen.eighteenandroid.domain.usecase.UserUseCase +import com.eighteen.eighteenandroid.domain.model.Tournament +import com.eighteen.eighteenandroid.domain.usecase.GetAnotherUserUseCase +import com.eighteen.eighteenandroid.domain.usecase.GetAuthTokenFlowUseCase +import com.eighteen.eighteenandroid.domain.usecase.GetPopularUserUseCase +import com.eighteen.eighteenandroid.domain.usecase.UserLikeUseCase +import com.eighteen.eighteenandroid.presentation.common.ModelState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - private val userUseCase: UserUseCase + private val resourceProvider: ResourceProvider, + private val getAnotherUserUseCase: GetAnotherUserUseCase, + private val getPopularUserUseCase: GetPopularUserUseCase, + private val getAuthTokenFlowUseCase: GetAuthTokenFlowUseCase, + private val userLikeUseCase: UserLikeUseCase ) : ViewModel() { var popularUserPosition = 0 - var pageScrollPosition = 0 - private val _userData = MutableLiveData>() - val userData: LiveData> = _userData + /** 메인화면 전체 데이터 */ + private val _mainItemStateFlow = + MutableStateFlow>>(ModelState.Empty()) + val mainItemStateFlow: StateFlow>> + get() = _mainItemStateFlow.asStateFlow() - private val _mainItems = MutableLiveData>() - val mainItems: LiveData> - get() = _mainItems + /** 남은 페이지 목록 */ + private val _pageNumList = MutableLiveData>() + val pageNumList: LiveData> + get() = _pageNumList + + /** 좋아요 */ + private val _userLikeStateFlow = MutableStateFlow>(ModelState.Empty()) + val userLikeStateFlow: StateFlow> + get() = _userLikeStateFlow.asStateFlow() + + /** 좋아요 취소 */ + private val _userLikeCancelStateFlow = MutableStateFlow>(ModelState.Empty()) + val userLikeCancelStateFlow: StateFlow> + get() = _userLikeCancelStateFlow.asStateFlow() init { - updateMain() + initMain(Tag.ALL) } - private fun updateMain() { + fun initMain(category: Tag) = viewModelScope.launch { val items = mutableListOf() + _mainItemStateFlow.value = ModelState.Loading() + + delay(1500) + // AuthToken 여부 확인 후 Fetch + val authToken = getAuthTokenFlowUseCase.invoke().firstOrNull() + + // 인기 Teen + val popularTeenInitialState = if(authToken != null) { + getPopularUser(category, USER_TYPE_SIGNIN) + } else { + getPopularUser(category, USER_TYPE_GUEST) + } + + popularTeenInitialState.onSuccess { + with(items) { + add(MainItem.HeaderView(resourceProvider.getString(R.string.main_today_teen))) + add(MainItem.UserListView(it)) + add(MainItem.DividerView) + } + }.onFailure { + _mainItemStateFlow.value = ModelState.Error(throwable = it) + } + + with(items) { + add(MainItem.HeaderView(resourceProvider.getString(R.string.main_about_teen))) + add(MainItem.AboutTeenListView( + listOf( + AboutTeen("Teen", "친구들의 프로필을 투표해보세요!"), + AboutTeen("토너먼트", "투표 결과를 한 눈에 볼 수 있어요!"), + AboutTeen("채팅", "채팅을 통해 친구들과 소통해보세요!"), + AboutTeen("나만의 Teen", "나만의 프로필을 등록해보세요!") + ) + )) + add(MainItem.DividerView) + } + + // TODO. 토너먼트 목록 API 연결 + // val tournamentInitialState + + with(items) { + add(MainItem.HeaderWithMoreView(resourceProvider.getString(R.string.main_tournament_in_progress))) + add(MainItem.TournamentListView( + listOf( + Tournament.Exercise, + Tournament.Study + ) + )) + add(MainItem.DividerView) + } + + // 또 다른 Teen + val anotherTeenInitialState = if (authToken != null) { + getAnotherUser(category, USER_TYPE_SIGNIN, 0) + } else { + getAnotherUser(category, USER_TYPE_GUEST, 0) + } + + anotherTeenInitialState.onSuccess { + if (it.totalPageCount > 1) { + val pages = mutableListOf() + + // 첫 번째 페이지(0)를 제외한 페이지 목록 + for (page in 1 until it.totalPageCount) { + pages.add(page) + } + + _pageNumList.postValue(pages) + } + + with(items) { + add(MainItem.HeaderView(resourceProvider.getString(R.string.main_another_teen))) + + it.users.forEach { user -> + add(MainItem.UserView(user)) + } + } + }.onFailure { e -> + _mainItemStateFlow.value = ModelState.Error(throwable = e) + } - // 둘 중 하나 에러나면 다시 전체 가져오기 + // 모두 가져오는 데에 성공하면 + if(anotherTeenInitialState.isSuccess && popularTeenInitialState.isSuccess) { + _mainItemStateFlow.value = ModelState.Success(items) + } else { + _mainItemStateFlow.value = ModelState.Error(throwable = ApiException.Unknown) + } + } + /** 또 다른 Teen 무한 스크롤, 다음 페이지 유저 정보 가져오기 */ + fun requestNextPage(category: Tag, page: Int) { viewModelScope.launch { - fetchUserData() -// val userDataState = fetchUserData().await() -// val aboutTeenDataState = fetchUserData().await() - // 토너먼트 - // 1페이지 - -// if( userDataState is ModelState.Success && aboutTeenDataState is ModelState.Success ) { -// -// // updateMain -// } else { -// // 에러화면 -// } - - _mainItems.value = items + val items = _mainItemStateFlow.value.data?.toMutableList() // 기존 값 + + // 로딩 시작 + _mainItemStateFlow.value = ModelState.Loading() + delay(1000) + + // AuthToken 여부 확인 후 Fetch + val authToken = getAuthTokenFlowUseCase.invoke().firstOrNull() + val anotherTeenInitialState = if (authToken != null) { + getAnotherUser(category, USER_TYPE_SIGNIN, page) + } else { + getAnotherUser(category, USER_TYPE_GUEST, page) + } + + anotherTeenInitialState + .onSuccess { + it.users.forEach { user -> + items?.add( + MainItem.UserView(user) + ) + } + + _mainItemStateFlow.value = ModelState.Success(items) + } + .onFailure { e -> + _mainItemStateFlow.value = ModelState.Error(throwable = e) + } } } - /*fun 무한_스크롤() { - val items = mainItems.value // 기존 데이터 - items.add(새로 받아온 놈들) - _mainItems.value = items - }*/ + fun removePage(page: Int) { + val pages = _pageNumList.value + pages?.let { + it.remove(page) + _pageNumList.postValue(it) + } + } - private suspend fun fetchUserData() = viewModelScope.async { - userUseCase.invoke().onSuccess { -// ModelState.Success(it) - _userData.value = it - }.onFailure { e -> - Log.e(TAG, e.toString()) -// ModelState.Error(e) + private suspend fun getAnotherUser(category: Tag, userType: String, page: Int) = + viewModelScope.async { + getAnotherUserUseCase.invoke(category.name, userType, page) + }.await() + + private suspend fun getPopularUser(category: Tag, userType: String) = + viewModelScope.async { + getPopularUserUseCase.invoke(category.name, userType) + }.await() + + fun requestUserLike(isLike: Boolean, likedId: Int) { + viewModelScope.launch { + if (isLike) { // 좋아요 + _userLikeStateFlow.value = ModelState.Loading() + + postLikeUser(likedId).await() + .onSuccess { + _userLikeStateFlow.value = ModelState.Success(likedId) + } + .onFailure { + _userLikeStateFlow.value = ModelState.Error(throwable = it) + } + + } else { // 좋아요 취소 + _userLikeCancelStateFlow.value = ModelState.Loading() + + postLikeCancelUser(likedId).await() + .onSuccess { + _userLikeCancelStateFlow.value = ModelState.Success(likedId) + } + .onFailure { + _userLikeCancelStateFlow.value = ModelState.Error(throwable = it) + } + } } } + private fun postLikeUser(likedId: Int) = viewModelScope.async { + userLikeUseCase.invoke(true, likedId) + } + + private fun postLikeCancelUser(likedId: Int) = viewModelScope.async { + userLikeUseCase.invoke(false, likedId) + } + companion object { const val TAG = "MainViewModel" } diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/MainAdapter.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/MainAdapter.kt index a4ef5fd5..b6c099b0 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/MainAdapter.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/MainAdapter.kt @@ -4,6 +4,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageButton import androidx.core.view.doOnLayout import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.PagerSnapHelper @@ -15,13 +16,13 @@ import com.eighteen.eighteenandroid.databinding.ItemDividerBinding import com.eighteen.eighteenandroid.databinding.ItemMainAboutTeenListviewBinding import com.eighteen.eighteenandroid.databinding.ItemMainHeaderBinding import com.eighteen.eighteenandroid.databinding.ItemMainHeaderMoreBinding +import com.eighteen.eighteenandroid.databinding.ItemMainLoadingBinding import com.eighteen.eighteenandroid.databinding.ItemMainPopularTeenListviewBinding import com.eighteen.eighteenandroid.databinding.ItemMainTournamentListviewBinding import com.eighteen.eighteenandroid.databinding.ItemTeenBinding import com.eighteen.eighteenandroid.domain.model.MainItem import com.eighteen.eighteenandroid.domain.model.Tournament import com.eighteen.eighteenandroid.domain.model.User -import com.eighteen.eighteenandroid.presentation.common.dp2Px import com.eighteen.eighteenandroid.presentation.home.adapter.diffcallback.MainItemDiffCallBack interface MainAdapterListener { @@ -29,7 +30,7 @@ interface MainAdapterListener { fun onUserClicks(user: User) /** 유저 좋아요 클릭 */ - fun onUserLikeClicks(user: User) + fun onUserLikeClicks(likeBtn: ImageButton, user: User) /** 유저 채팅 클릭 */ fun onUserChatClicks(user: User) @@ -52,9 +53,6 @@ interface MainAdapterListener { /** 이전에 보여진 유저 포지션 저장*/ fun saveUserPosition(position: Int) - /** 현재 메인화면 스크롤 포지션 저장*/ - fun saveScrollPosition(position: Int) - fun startAutoScroll() fun stopAutoScroll() @@ -71,6 +69,7 @@ class MainAdapter( const val USER_VIEW = 5 const val ABOUT_TEEN_LIST = 6 const val TOURNAMENT_LIST = 7 + const val LOADING_VIEW = 8 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder { @@ -114,6 +113,11 @@ class MainAdapter( CommonViewHolder.TournamentListViewHolder(itemBinding, listener) } + LOADING_VIEW -> { + val itemBinding = ItemMainLoadingBinding.inflate(layoutInflater, parent, false) + CommonViewHolder.LoadingViewHolder(itemBinding) + } + else -> throw IllegalArgumentException("Invalid ViewType") } } @@ -148,6 +152,10 @@ class MainAdapter( (getItem(position) as? MainItem.DividerView)?.let { holder.bind(it) } } + is CommonViewHolder.LoadingViewHolder -> { + (getItem(position) as? MainItem.LoadingView)?.let { holder.bind((it)) } + } + else -> throw IllegalArgumentException("Invalid ViewHolder") } } @@ -161,6 +169,7 @@ class MainAdapter( is MainItem.AboutTeenListView -> ABOUT_TEEN_LIST is MainItem.TournamentListView -> TOURNAMENT_LIST is MainItem.UserView -> USER_VIEW + is MainItem.LoadingView -> LOADING_VIEW } } @@ -244,7 +253,7 @@ class MainAdapter( } popularUserAdapter.submitList(userListView?.userList) { - rvMainTeenPopularList.doOnLayout { + rvMainTeenPopularList.post { listener.scrollToPreviousUser() } } @@ -258,9 +267,6 @@ class MainAdapter( private val listener: MainAdapterListener ) : CommonViewHolder(binding) { fun bind(item: MainItem, itemCount: Int, position: Int) { - if(itemCount -1 == position) { - itemView.setPadding(0, 0, 0, context.dp2Px(60)) - } val userView = item as? MainItem.UserView with(binding) { userView?.let { @@ -269,16 +275,16 @@ class MainAdapter( tvSchool.text = user.userSchoolName tvName.text = userName Glide.with(context).load(user.userImage).into(imgTodayTeen) // 프로필 이미지 + btnLike.isSelected = user.likeStatus // 좋아요 버튼 Selector imgTodayTeen.setOnClickListener { listener.onUserClicks(user) - listener.saveScrollPosition(position) // 현재 메인화면 스크롤 position 저장 } btnChat.setOnClickListener { listener.onUserChatClicks(user) } btnLike.setOnClickListener { - listener.onUserLikeClicks(user) + listener.onUserLikeClicks(btnLike, user) } btnSetting.setOnClickListener { listener.onUserMoreClicks(btnSetting, user) @@ -286,6 +292,10 @@ class MainAdapter( } } } + + fun updateLikeBtn(isLike: Boolean) { + binding.btnLike.isSelected = isLike + } } class AboutTeenListViewHolder( @@ -327,9 +337,64 @@ class MainAdapter( // ... } } + + class LoadingViewHolder( + private val binding: ItemMainLoadingBinding + ): CommonViewHolder(binding) { + fun bind(item: MainItem) { + // ... + } + } } fun updateView(list: List) { - submitList(list) + submitList(list) { + listener.scrollToPreviousUser() + } + } + + fun addLoadingView(scrollToPosition: (Int) -> Unit) { + val currentList = currentList.toMutableList() + currentList.add(MainItem.LoadingView) + + submitList(currentList) { + scrollToPosition(currentList.size - 1) + } + } + + fun removeAllViews() { + submitList(null) + } + + fun removeLoadingView() { + val currentList = currentList.toMutableList() + if(currentList.lastIndex > 0) currentList.removeAt(currentList.lastIndex) + + submitList(currentList) + } + + fun updateUserLikeStatus(recyclerView: RecyclerView, userId: Int, isLike: Boolean) { + // 전체 아이템 중에서 해당 userId를 가진 유저 아이템 찾기 + val position = currentList.indexOfFirst { item -> + when (item) { + is MainItem.UserView -> item.user.userId == userId + else -> false + } + } + + if (position != -1) { + // 해당 위치의 ViewHolder 찾기 + val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) as? CommonViewHolder.UserViewHolder + + // ViewHolder가 현재 화면에 보이는 경우 직접 업데이트 + viewHolder?.updateLikeBtn(isLike) + + // 데이터도 업데이트 + val currentItem = currentList[position] as? MainItem.UserView + currentItem?.user?.likeStatus = isLike + + // 해당 포지션만 갱신 + notifyItemChanged(position) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/PopularUserAdapter.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/PopularUserAdapter.kt index 4eb0f9f9..a3959465 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/PopularUserAdapter.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/PopularUserAdapter.kt @@ -9,7 +9,6 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.eighteen.eighteenandroid.databinding.ItemPopularTeenBinding import com.eighteen.eighteenandroid.domain.model.User -import com.eighteen.eighteenandroid.presentation.common.dp2Px import com.eighteen.eighteenandroid.presentation.common.getScreenWidth import com.eighteen.eighteenandroid.presentation.home.adapter.diffcallback.UserDiffCallBack import kotlin.math.roundToInt @@ -53,6 +52,7 @@ class PopularUserAdapter( Glide.with(context).load(user.userImage).into(imgTodayTeen) tvName.text = "${user.userName}, ${user.userAge}" tvSchool.text = user.userSchoolName + btnLike.isSelected = user.likeStatus // 좋아요 상태 imgTodayTeen.setOnClickListener { mainAdapterListener.onUserClicks(user) @@ -63,7 +63,7 @@ class PopularUserAdapter( } btnLike.setOnClickListener { - mainAdapterListener.onUserLikeClicks(user) + mainAdapterListener.onUserLikeClicks(btnLike, user) } btnSetting.setOnClickListener { diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/MainItemDiffCallBack.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/MainItemDiffCallBack.kt index bd8735a3..3253ac43 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/MainItemDiffCallBack.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/MainItemDiffCallBack.kt @@ -19,7 +19,7 @@ class MainItemDiffCallBack: DiffUtil.ItemCallback() { } oldItem is MainItem.DividerView && newItem is MainItem.DividerView -> { - false + true } oldItem is MainItem.AboutTeenListView && newItem is MainItem.AboutTeenListView -> { @@ -30,6 +30,14 @@ class MainItemDiffCallBack: DiffUtil.ItemCallback() { oldItem.tournamentList == newItem.tournamentList } + oldItem is MainItem.UserView && newItem is MainItem.UserView -> { + oldItem.user.userId == newItem.user.userId || oldItem.user.uniqueId == newItem.user.uniqueId + } + + oldItem is MainItem.LoadingView && newItem is MainItem.LoadingView -> { + true + } + else -> false } } diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/UserDiffCallBack.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/UserDiffCallBack.kt index 9e6471f5..3414da6d 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/UserDiffCallBack.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/home/adapter/diffcallback/UserDiffCallBack.kt @@ -5,7 +5,7 @@ import com.eighteen.eighteenandroid.domain.model.User class UserDiffCallBack() : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { - return oldItem.userId == newItem.userId + return oldItem.uniqueId == newItem.uniqueId || oldItem.userId == newItem.userId } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { diff --git a/app/src/main/java/com/eighteen/eighteenandroid/presentation/teen/TeenListFragment.kt b/app/src/main/java/com/eighteen/eighteenandroid/presentation/teen/TeenListFragment.kt index afd2fa32..c03c2267 100644 --- a/app/src/main/java/com/eighteen/eighteenandroid/presentation/teen/TeenListFragment.kt +++ b/app/src/main/java/com/eighteen/eighteenandroid/presentation/teen/TeenListFragment.kt @@ -58,28 +58,31 @@ class TeenListFragment: BaseFragment(FragmentTeenListBi adapter.submitList( listOf( User( + userId = 1, userImage = "https://image.blip.kr/v1/file/021ec61ff1c9936943383b84236a0e69", - userId = "1", + uniqueId = "1", userName = "김 에스더", userAge = "16", userSchoolName = "서울 중학교", - tag = "운동" + likeStatus = true ), User( + userId = 2, userImage = "https://cdn.newsculture.press/news/photo/202308/529742_657577_5726.jpg", - userId = "2", + uniqueId = "2", userName = "김 에스더", userAge = "16", userSchoolName = "부천 중학교", - tag = "스터디" + likeStatus = false ), User( + userId = 3, userImage = "https://mblogthumb-phinf.pstatic.net/MjAyMTEwMzFfMTY1/MDAxNjM1NjUzMTI2NjI3.xXYQteLLoWLKcR9YnXS0Hk_y-DInauMzF25g7FxlcScg.2Y-neBBMVoP2IhcwzX2Zy2HB2d8EnM_cY76FVLuk_1Yg.JPEG.ssun2415/IMG_4148.jpg?type=w800", - userId = "3", + uniqueId = "3", userName = "김 에스더", userAge = "16", userSchoolName = "인천 중학교", - tag = "프로젝트" + likeStatus = false ) ) ) diff --git a/app/src/main/res/drawable/bg_btn_like_selector.xml b/app/src/main/res/drawable/bg_btn_like_selector.xml new file mode 100644 index 00000000..f676e322 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_like_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_empty_heart.xml b/app/src/main/res/drawable/bg_empty_heart.xml new file mode 100644 index 00000000..29023e4d --- /dev/null +++ b/app/src/main/res/drawable/bg_empty_heart.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_full_heart.xml b/app/src/main/res/drawable/bg_full_heart.xml new file mode 100644 index 00000000..8cf08322 --- /dev/null +++ b/app/src/main/res/drawable/bg_full_heart.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_main_loading.xml b/app/src/main/res/layout/item_main_loading.xml new file mode 100644 index 00000000..99e64542 --- /dev/null +++ b/app/src/main/res/layout/item_main_loading.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_popular_teen.xml b/app/src/main/res/layout/item_popular_teen.xml index 2f7a5e8f..133915c4 100644 --- a/app/src/main/res/layout/item_popular_teen.xml +++ b/app/src/main/res/layout/item_popular_teen.xml @@ -66,8 +66,7 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_marginEnd="16dp" - android:background="@drawable/bg_oval_main_color" - android:src="@drawable/ic_empty_heart" + android:background="@drawable/bg_btn_like_selector" app:layout_constraintBottom_toTopOf="@id/btn_setting" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/item_teen.xml b/app/src/main/res/layout/item_teen.xml index 8bbcfd1c..e8b7a942 100644 --- a/app/src/main/res/layout/item_teen.xml +++ b/app/src/main/res/layout/item_teen.xml @@ -74,8 +74,7 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_marginEnd="16dp" - android:background="@drawable/bg_oval_main_color" - android:src="@drawable/ic_empty_heart" + android:background="@drawable/bg_btn_like_selector" app:layout_constraintBottom_toTopOf="@id/btn_setting" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/raw/loading.json b/app/src/main/res/raw/loading.json new file mode 100644 index 00000000..acd137a2 --- /dev/null +++ b/app/src/main/res/raw/loading.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":60,"ip":0,"op":61,"w":600,"h":600,"nm":"ios-spinner","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":2,"ty":4,"nm":"2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[100]},{"t":60,"s":[8]}],"ix":11},"r":{"a":0,"k":-150,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":3,"ty":4,"nm":"3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[16]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":60,"s":[16]}],"ix":11},"r":{"a":0,"k":-120,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":4,"ty":4,"nm":"4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[24]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[100]},{"t":60,"s":[24]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":5,"ty":4,"nm":"5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[32]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[100]},{"t":60,"s":[32]}],"ix":11},"r":{"a":0,"k":-60,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":6,"ty":4,"nm":"6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[40]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[100]},{"t":60,"s":[40]}],"ix":11},"r":{"a":0,"k":-30,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":7,"ty":4,"nm":"7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[48]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[100]},{"t":60,"s":[48]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":8,"ty":4,"nm":"8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[56]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":34,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[100]},{"t":60,"s":[56]}],"ix":11},"r":{"a":0,"k":-330,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":9,"ty":4,"nm":"9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[64]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":39,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[100]},{"t":60,"s":[64]}],"ix":11},"r":{"a":0,"k":-300,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":10,"ty":4,"nm":"10","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[72]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":44,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"t":60,"s":[72]}],"ix":11},"r":{"a":0,"k":-270,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":11,"ty":4,"nm":"11","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[100]},{"t":60,"s":[80]}],"ix":11},"r":{"a":0,"k":-240,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":12,"ty":4,"nm":"12","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[88]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":54,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":55,"s":[100]},{"t":60,"s":[88]}],"ix":11},"r":{"a":0,"k":-210,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[13,22],[13,124]],"o":[[13,22],[13,124]],"v":[[13,22],[13,124]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[0.1804,0.0118,0.898,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[11,-82],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":61,"st":0,"bm":0,"completed":true}],"markers":[],"__complete":true} \ No newline at end of file