diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c0a6718e..337e58fb3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -134,6 +134,9 @@ + +) { + @Serializable + data class User( + @SerialName("nickname") + val nickname: String, + @SerialName("level") + val level: Int, + @SerialName("levelPercent") + val levelPercent: Int, + @SerialName("latestStamp") + val latestStamp: String, + @SerialName("userId") + val userId: Int + ) + + @Serializable + data class CourseData( + @SerialName("publicCourseId") + val publicCourseId: Int, + @SerialName("courseId") + val courseId: Int, + @SerialName("title") + val title: String, + @SerialName("image") + val image: String, + @SerialName("departure") + val departure: Departure, + @SerialName("scrapTF") + val scrapTF: Boolean, + ) { + @Serializable + data class Departure( + @SerialName("region") + val region: String, + @SerialName("city") + val city: String, + @SerialName("town") + val town: String, + @SerialName("name") + val name: String?, + @SerialName("detail") + val detail: String? + ) + } + + fun toUserProfile(): UserProfile { + val userCourseLists: List = courses.map { course -> + UserCourse( + publicCourseId = course.publicCourseId, + courseId = course.courseId, + title = course.title, + image = course.image, + departure = Departure( + region = course.departure.region, + city = course.departure.city, + town = course.departure.town, + detail = course.departure.detail, + name = course.departure.name ?: "" + ), + scrapTF = course.scrapTF + ) + } + + return UserProfile( + nickname = user.nickname, + level = user.level, + levelPercent = user.levelPercent, + latestStamp = user.latestStamp, + courseData = userCourseLists + ) + } + +} diff --git a/app/src/main/java/com/runnect/runnect/data/dto/response/ResponsePostScrap.kt b/app/src/main/java/com/runnect/runnect/data/dto/response/ResponsePostScrap.kt new file mode 100644 index 000000000..cd5d3b0ae --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/data/dto/response/ResponsePostScrap.kt @@ -0,0 +1,14 @@ +package com.runnect.runnect.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePostScrap( + @SerialName("publicCourseId") + val publicCourseId: Long, + @SerialName("scrapCount") + val scrapCount: Long, + @SerialName("scrapTF") + val scrapTF: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/data/repository/CourseRepositoryImpl.kt b/app/src/main/java/com/runnect/runnect/data/repository/CourseRepositoryImpl.kt index c8622db4c..540a03cd1 100644 --- a/app/src/main/java/com/runnect/runnect/data/repository/CourseRepositoryImpl.kt +++ b/app/src/main/java/com/runnect/runnect/data/repository/CourseRepositoryImpl.kt @@ -12,6 +12,7 @@ import com.runnect.runnect.data.dto.response.ResponsePostMyDrawCourse import com.runnect.runnect.data.dto.response.ResponsePostMyHistory import com.runnect.runnect.data.dto.response.ResponsePutMyDrawCourse import com.runnect.runnect.data.dto.response.ResponsePostDiscoverUpload +import com.runnect.runnect.data.dto.response.ResponsePostScrap import com.runnect.runnect.data.source.remote.RemoteCourseDataSource import com.runnect.runnect.domain.entity.DiscoverSearchCourse import com.runnect.runnect.domain.entity.DiscoverMultiViewItem.* @@ -100,7 +101,7 @@ class CourseRepositoryImpl @Inject constructor(private val remoteCourseDataSourc override suspend fun postCourseScrap( requestPostCourseScrap: RequestPostCourseScrap - ): Result = runCatching { + ): Result = runCatching { remoteCourseDataSource.postCourseScrap(requestPostCourseScrap = requestPostCourseScrap).data } } \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/runnect/runnect/data/repository/UserRepositoryImpl.kt index 2b6350af5..8c0bf1081 100644 --- a/app/src/main/java/com/runnect/runnect/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/runnect/runnect/data/repository/UserRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.runnect.runnect.data.repository import com.runnect.runnect.data.dto.HistoryInfoDTO +import com.runnect.runnect.domain.entity.UserProfile import com.runnect.runnect.data.dto.UserUploadCourseDTO import com.runnect.runnect.data.dto.request.RequestDeleteHistory import com.runnect.runnect.data.dto.request.RequestDeleteUploadCourse @@ -9,9 +10,9 @@ import com.runnect.runnect.data.dto.request.RequestPatchNickName import com.runnect.runnect.data.dto.response.ResponseDeleteHistory import com.runnect.runnect.data.dto.response.ResponseDeleteUploadCourse import com.runnect.runnect.data.dto.response.ResponseDeleteUser +import com.runnect.runnect.data.dto.response.ResponseGetUser import com.runnect.runnect.data.dto.response.ResponsePatchHistoryTitle import com.runnect.runnect.data.dto.response.ResponsePatchUserNickName -import com.runnect.runnect.data.dto.response.ResponseGetUser import com.runnect.runnect.data.source.remote.RemoteUserDataSource import com.runnect.runnect.domain.repository.UserRepository import com.runnect.runnect.util.extension.toData @@ -36,6 +37,11 @@ class UserRepositoryImpl @Inject constructor(private val remoteUserDataSource: R .toMutableList() } + override suspend fun getUserProfile(userId: Int): Result = + runCatching { + remoteUserDataSource.getUserProfile(userId).data?.toUserProfile() + } + override suspend fun putDeleteUploadCourse( requestDeleteUploadCourse: RequestDeleteUploadCourse ): Result = runCatching { diff --git a/app/src/main/java/com/runnect/runnect/data/service/CourseService.kt b/app/src/main/java/com/runnect/runnect/data/service/CourseService.kt index 64a8225ad..aa579c61b 100644 --- a/app/src/main/java/com/runnect/runnect/data/service/CourseService.kt +++ b/app/src/main/java/com/runnect/runnect/data/service/CourseService.kt @@ -1,10 +1,10 @@ package com.runnect.runnect.data.service +import com.runnect.runnect.data.dto.request.RequestPatchPublicCourse import com.runnect.runnect.data.dto.request.RequestPostCourseScrap +import com.runnect.runnect.data.dto.request.RequestPostPublicCourse import com.runnect.runnect.data.dto.request.RequestPostRunningHistory import com.runnect.runnect.data.dto.request.RequestPutMyDrawCourse -import com.runnect.runnect.data.dto.request.RequestPatchPublicCourse -import com.runnect.runnect.data.dto.request.RequestPostPublicCourse import com.runnect.runnect.data.dto.response.* import com.runnect.runnect.data.dto.response.base.BaseResponse import okhttp3.MultipartBody @@ -25,7 +25,7 @@ interface CourseService { @POST("/api/scrap") suspend fun postCourseScrap( @Body requestPostCourseScrap: RequestPostCourseScrap, - ): BaseResponse + ): BaseResponse @GET("/api/public-course/search?") suspend fun getCourseSearch( diff --git a/app/src/main/java/com/runnect/runnect/data/service/UserService.kt b/app/src/main/java/com/runnect/runnect/data/service/UserService.kt index 62fb27610..780a22bf9 100644 --- a/app/src/main/java/com/runnect/runnect/data/service/UserService.kt +++ b/app/src/main/java/com/runnect/runnect/data/service/UserService.kt @@ -48,4 +48,10 @@ interface UserService { @DELETE("api/user") suspend fun deleteUser(): ResponseDeleteUser + + // 유저 프로필 조회 + @GET("/api/user/{profileUserId}") + suspend fun getUserProfile( + @Path("profileUserId") userId: Int, + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteCourseDataSource.kt b/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteCourseDataSource.kt index fecd2ff75..ffb83942d 100644 --- a/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteCourseDataSource.kt +++ b/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteCourseDataSource.kt @@ -1,16 +1,17 @@ package com.runnect.runnect.data.source.remote -import com.runnect.runnect.data.service.CourseService +import com.runnect.runnect.data.dto.request.RequestPatchPublicCourse import com.runnect.runnect.data.dto.request.RequestPostCourseScrap +import com.runnect.runnect.data.dto.request.RequestPostPublicCourse import com.runnect.runnect.data.dto.request.RequestPostRunningHistory import com.runnect.runnect.data.dto.request.RequestPutMyDrawCourse -import com.runnect.runnect.data.dto.request.RequestPatchPublicCourse -import com.runnect.runnect.data.dto.request.RequestPostPublicCourse import com.runnect.runnect.data.dto.response.ResponseGetCourseDetail import com.runnect.runnect.data.dto.response.ResponseGetDiscoverMarathon -import com.runnect.runnect.data.dto.response.ResponsePatchPublicCourse import com.runnect.runnect.data.dto.response.ResponseGetDiscoverRecommend +import com.runnect.runnect.data.dto.response.ResponsePatchPublicCourse +import com.runnect.runnect.data.dto.response.ResponsePostScrap import com.runnect.runnect.data.dto.response.base.BaseResponse +import com.runnect.runnect.data.service.CourseService import okhttp3.MultipartBody import okhttp3.RequestBody import javax.inject.Inject @@ -27,7 +28,7 @@ class RemoteCourseDataSource @Inject constructor( ): BaseResponse = courseService.getRecommendCourse(pageNo = pageNo, ordering = ordering) - suspend fun postCourseScrap(requestPostCourseScrap: RequestPostCourseScrap): BaseResponse = + suspend fun postCourseScrap(requestPostCourseScrap: RequestPostCourseScrap): BaseResponse = courseService.postCourseScrap(requestPostCourseScrap) suspend fun getCourseSearch(keyword: String) = courseService.getCourseSearch(keyword) diff --git a/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteUserDataSource.kt b/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteUserDataSource.kt index 0964340c5..19d9b64e0 100644 --- a/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteUserDataSource.kt +++ b/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteUserDataSource.kt @@ -1,12 +1,21 @@ package com.runnect.runnect.data.source.remote -import com.runnect.runnect.data.service.UserService import com.runnect.runnect.data.dto.request.RequestDeleteHistory import com.runnect.runnect.data.dto.request.RequestDeleteUploadCourse import com.runnect.runnect.data.dto.request.RequestPatchHistoryTitle import com.runnect.runnect.data.dto.request.RequestPatchNickName -import com.runnect.runnect.data.dto.response.* +import com.runnect.runnect.data.dto.response.ResponseDeleteHistory +import com.runnect.runnect.data.dto.response.ResponseDeleteUploadCourse +import com.runnect.runnect.data.dto.response.ResponseDeleteUser +import com.runnect.runnect.data.dto.response.ResponseGetMyHistory +import com.runnect.runnect.data.dto.response.ResponseGetMyStamp +import com.runnect.runnect.data.dto.response.ResponseGetUser +import com.runnect.runnect.data.dto.response.ResponseGetUserProfile +import com.runnect.runnect.data.dto.response.ResponseGetUserUploadCourse +import com.runnect.runnect.data.dto.response.ResponsePatchHistoryTitle +import com.runnect.runnect.data.dto.response.ResponsePatchUserNickName import com.runnect.runnect.data.dto.response.base.BaseResponse +import com.runnect.runnect.data.service.UserService import javax.inject.Inject class RemoteUserDataSource @Inject constructor(private val userService: UserService) { @@ -16,7 +25,11 @@ class RemoteUserDataSource @Inject constructor(private val userService: UserServ suspend fun getMyStamp(): ResponseGetMyStamp = userService.getMyStamp() suspend fun getRecord(): ResponseGetMyHistory = userService.getRecord() - suspend fun getUserUploadCourse(): ResponseGetUserUploadCourse = userService.getUserUploadCourse() + suspend fun getUserUploadCourse(): ResponseGetUserUploadCourse = + userService.getUserUploadCourse() + + suspend fun getUserProfile(userId: Int): BaseResponse = + userService.getUserProfile(userId) suspend fun putDeleteUploadCourse( requestDeleteUploadCourse: RequestDeleteUploadCourse diff --git a/app/src/main/java/com/runnect/runnect/domain/entity/CourseDetail.kt b/app/src/main/java/com/runnect/runnect/domain/entity/CourseDetail.kt index ca0476e67..4fe5a6cb1 100644 --- a/app/src/main/java/com/runnect/runnect/domain/entity/CourseDetail.kt +++ b/app/src/main/java/com/runnect/runnect/domain/entity/CourseDetail.kt @@ -15,4 +15,5 @@ data class CourseDetail( val path: List>, val distance: String, val departure: String, + val userId: Int ) diff --git a/app/src/main/java/com/runnect/runnect/domain/entity/UserProfile.kt b/app/src/main/java/com/runnect/runnect/domain/entity/UserProfile.kt new file mode 100644 index 000000000..99276137e --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/domain/entity/UserProfile.kt @@ -0,0 +1,26 @@ +package com.runnect.runnect.domain.entity + +data class UserProfile( + val nickname: String, + val level: Int, + val levelPercent: Int, + val latestStamp: String, + val courseData: List +) + +data class UserCourse( + val publicCourseId: Int, + val courseId: Int, + val title: String, + val image: String, + val departure: Departure, + var scrapTF: Boolean, +) + +data class Departure( + val region: String, + val city: String, + val town: String, + val detail: String?, + val name: String +) diff --git a/app/src/main/java/com/runnect/runnect/domain/repository/CourseRepository.kt b/app/src/main/java/com/runnect/runnect/domain/repository/CourseRepository.kt index 02665c5e1..ec56b0e9b 100644 --- a/app/src/main/java/com/runnect/runnect/domain/repository/CourseRepository.kt +++ b/app/src/main/java/com/runnect/runnect/domain/repository/CourseRepository.kt @@ -10,12 +10,13 @@ import com.runnect.runnect.data.dto.response.ResponseGetMyDrawDetail import com.runnect.runnect.data.dto.response.ResponsePostDiscoverUpload import com.runnect.runnect.data.dto.response.ResponsePostMyDrawCourse import com.runnect.runnect.data.dto.response.ResponsePostMyHistory +import com.runnect.runnect.data.dto.response.ResponsePostScrap import com.runnect.runnect.data.dto.response.ResponsePutMyDrawCourse import com.runnect.runnect.domain.entity.CourseDetail +import com.runnect.runnect.domain.entity.DiscoverMultiViewItem.MarathonCourse import com.runnect.runnect.domain.entity.DiscoverSearchCourse -import com.runnect.runnect.domain.entity.DiscoverMultiViewItem.* -import com.runnect.runnect.domain.entity.RecommendCoursePagingData import com.runnect.runnect.domain.entity.EditableCourseDetail +import com.runnect.runnect.domain.entity.RecommendCoursePagingData import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Response @@ -23,7 +24,10 @@ import retrofit2.Response interface CourseRepository { suspend fun getMarathonCourse(): Result?> - suspend fun getRecommendCourse(pageNo: String, ordering: String): Result + suspend fun getRecommendCourse( + pageNo: String, + ordering: String + ): Result suspend fun getCourseSearch(keyword: String): Result?> @@ -40,6 +44,7 @@ interface CourseRepository { suspend fun uploadCourse( image: MultipartBody.Part, courseCreateRequestDto: RequestBody ): Response + suspend fun getCourseDetail(publicCourseId: Int): Result suspend fun patchPublicCourse( @@ -47,5 +52,5 @@ interface CourseRepository { requestPatchPublicCourse: RequestPatchPublicCourse ): Result - suspend fun postCourseScrap(requestPostCourseScrap: RequestPostCourseScrap): Result + suspend fun postCourseScrap(requestPostCourseScrap: RequestPostCourseScrap): Result } \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/domain/repository/UserRepository.kt b/app/src/main/java/com/runnect/runnect/domain/repository/UserRepository.kt index 7f8f4db02..bb2713628 100644 --- a/app/src/main/java/com/runnect/runnect/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/runnect/runnect/domain/repository/UserRepository.kt @@ -1,12 +1,18 @@ package com.runnect.runnect.domain.repository import com.runnect.runnect.data.dto.HistoryInfoDTO +import com.runnect.runnect.domain.entity.UserProfile import com.runnect.runnect.data.dto.UserUploadCourseDTO import com.runnect.runnect.data.dto.request.RequestDeleteHistory import com.runnect.runnect.data.dto.request.RequestDeleteUploadCourse import com.runnect.runnect.data.dto.request.RequestPatchHistoryTitle import com.runnect.runnect.data.dto.request.RequestPatchNickName -import com.runnect.runnect.data.dto.response.* +import com.runnect.runnect.data.dto.response.ResponseDeleteHistory +import com.runnect.runnect.data.dto.response.ResponseDeleteUploadCourse +import com.runnect.runnect.data.dto.response.ResponseDeleteUser +import com.runnect.runnect.data.dto.response.ResponseGetUser +import com.runnect.runnect.data.dto.response.ResponsePatchHistoryTitle +import com.runnect.runnect.data.dto.response.ResponsePatchUserNickName interface UserRepository { suspend fun getUserInfo(): ResponseGetUser @@ -19,6 +25,8 @@ interface UserRepository { suspend fun getUserUploadCourse(): MutableList + suspend fun getUserProfile(userId: Int): Result + suspend fun putDeleteUploadCourse( requestDeleteUploadCourse: RequestDeleteUploadCourse ): Result diff --git a/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailActivity.kt index 1a3a13e38..59c90fde6 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailActivity.kt @@ -29,21 +29,26 @@ import com.runnect.runnect.data.dto.CourseData import com.runnect.runnect.databinding.ActivityCourseDetailBinding import com.runnect.runnect.domain.entity.CourseDetail import com.runnect.runnect.domain.entity.EditableCourseDetail -import com.runnect.runnect.presentation.discover.model.EditableDiscoverCourse import com.runnect.runnect.presentation.MainActivity import com.runnect.runnect.presentation.countdown.CountDownActivity -import com.runnect.runnect.presentation.detail.CourseDetailRootScreen.* +import com.runnect.runnect.presentation.detail.CourseDetailRootScreen.COURSE_DISCOVER +import com.runnect.runnect.presentation.detail.CourseDetailRootScreen.COURSE_DISCOVER_SEARCH +import com.runnect.runnect.presentation.detail.CourseDetailRootScreen.COURSE_STORAGE_SCRAP +import com.runnect.runnect.presentation.detail.CourseDetailRootScreen.MY_PAGE_UPLOAD_COURSE import com.runnect.runnect.presentation.discover.DiscoverFragment.Companion.EXTRA_EDITABLE_DISCOVER_COURSE +import com.runnect.runnect.presentation.discover.model.EditableDiscoverCourse import com.runnect.runnect.presentation.discover.search.DiscoverSearchActivity import com.runnect.runnect.presentation.login.LoginActivity import com.runnect.runnect.presentation.mypage.upload.MyUploadActivity +import com.runnect.runnect.presentation.profile.ProfileActivity import com.runnect.runnect.presentation.state.UiStateV2 import com.runnect.runnect.util.custom.dialog.CommonDialogFragment import com.runnect.runnect.util.custom.dialog.CommonDialogText -import com.runnect.runnect.util.custom.popup.PopupItem import com.runnect.runnect.util.custom.dialog.RequireLoginDialogFragment +import com.runnect.runnect.util.custom.popup.PopupItem import com.runnect.runnect.util.custom.popup.RunnectPopupMenu import com.runnect.runnect.util.custom.toast.RunnectToast +import com.runnect.runnect.util.extension.applyScreenEnterAnimation import com.runnect.runnect.util.extension.applyScreenExitAnimation import com.runnect.runnect.util.extension.getCompatibleSerializableExtra import com.runnect.runnect.util.extension.getStampResId @@ -51,7 +56,8 @@ import com.runnect.runnect.util.extension.hideKeyboard import com.runnect.runnect.util.extension.showSnackbar import com.runnect.runnect.util.extension.showToast import com.runnect.runnect.util.extension.showWebBrowser -import com.runnect.runnect.util.mode.ScreenMode.* +import com.runnect.runnect.util.mode.ScreenMode.EditMode +import com.runnect.runnect.util.mode.ScreenMode.ReadOnlyMode import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber @@ -129,6 +135,15 @@ class CourseDetailActivity : applyScreenExitAnimation() } + private fun navigateToUserProfileWithBundle() { + Intent(this@CourseDetailActivity, ProfileActivity::class.java).apply { + putExtra(EXTRA_COURSE_USER_ID, courseDetail.userId) + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + startActivity(this) + } + applyScreenEnterAnimation() + } + private fun handleBackButtonByCurrentScreenMode() { when (viewModel.currentScreenMode) { is ReadOnlyMode -> navigateToPreviousScreen() @@ -136,6 +151,16 @@ class CourseDetailActivity : } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + intent?.let { newIntent -> + newIntent.getCompatibleSerializableExtra(EXTRA_ROOT_SCREEN) + ?.let { rootScreen = it } + publicCourseId = newIntent.getIntExtra(EXTRA_PUBLIC_COURSE_ID, 0) + getCourseDetail() + } + } + private fun navigateToPreviousScreen() { if (isFromDeepLink) { navigateToMainScreenWithBundle() @@ -178,6 +203,7 @@ class CourseDetailActivity : initScrapButtonClickListener() initStartRunButtonClickListener() initEditFinishButtonClickListener() + initUserInfoClickListener() initShareButtonClickListener() initShowMoreButtonClickListener() @@ -215,6 +241,12 @@ class CourseDetailActivity : } } + private fun initUserInfoClickListener() { + binding.constCourseDetailUserInfo.setOnClickListener { + navigateToUserProfileWithBundle() + } + } + // todo: 함수를 더 작게 분리하는 게 좋을 거 같아요! @우남 private fun sendKakaoLink(title: String, desc: String, image: String) { // 메시지 템플릿 만들기 (피드형) @@ -608,6 +640,7 @@ class CourseDetailActivity : private const val EXTRA_COURSE_DATA = "CourseData" private const val EXTRA_FRAGMENT_REPLACEMENT_DIRECTION = "fragmentReplacementDirection" private const val EXTRA_FROM_COURSE_DETAIL = "fromCourseDetail" + private const val EXTRA_COURSE_USER_ID = "courseUserId" private const val POPUP_MENU_X_OFFSET = 17 private const val POPUP_MENU_Y_OFFSET = -10 diff --git a/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailViewModel.kt b/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailViewModel.kt index 400a5ca05..895cd711b 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailViewModel.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/detail/CourseDetailViewModel.kt @@ -5,14 +5,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.map import androidx.lifecycle.viewModelScope -import com.runnect.runnect.data.dto.request.RequestPostCourseScrap import com.runnect.runnect.data.dto.request.RequestDeleteUploadCourse import com.runnect.runnect.data.dto.request.RequestPatchPublicCourse +import com.runnect.runnect.data.dto.request.RequestPostCourseScrap import com.runnect.runnect.data.dto.response.ResponseDeleteUploadCourse -import com.runnect.runnect.domain.repository.CourseRepository -import com.runnect.runnect.domain.repository.UserRepository +import com.runnect.runnect.data.dto.response.ResponsePostScrap import com.runnect.runnect.domain.entity.CourseDetail import com.runnect.runnect.domain.entity.EditableCourseDetail +import com.runnect.runnect.domain.repository.CourseRepository +import com.runnect.runnect.domain.repository.UserRepository import com.runnect.runnect.presentation.state.UiStateV2 import com.runnect.runnect.util.mode.ScreenMode import dagger.hilt.android.lifecycle.HiltViewModel @@ -37,8 +38,8 @@ class CourseDetailViewModel @Inject constructor( val courseDeleteState: LiveData> get() = _courseDeleteState - private var _courseScrapState = MutableLiveData>() - val courseScrapState: LiveData> + private var _courseScrapState = MutableLiveData>() + val courseScrapState: LiveData> get() = _courseScrapState // 플래그 변수 diff --git a/app/src/main/java/com/runnect/runnect/presentation/discover/DiscoverViewModel.kt b/app/src/main/java/com/runnect/runnect/presentation/discover/DiscoverViewModel.kt index f76fe6d8c..e6c5e129f 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/discover/DiscoverViewModel.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/discover/DiscoverViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.runnect.runnect.data.dto.request.RequestPostCourseScrap +import com.runnect.runnect.data.dto.response.ResponsePostScrap import com.runnect.runnect.domain.entity.DiscoverMultiViewItem import com.runnect.runnect.domain.entity.DiscoverMultiViewItem.* import com.runnect.runnect.domain.entity.DiscoverBanner @@ -39,8 +40,8 @@ class DiscoverViewModel @Inject constructor( val nextPageState: LiveData>> get() = _nextPageState - private val _courseScrapState = MutableLiveData>() - val courseScrapState: LiveData> + private val _courseScrapState = MutableLiveData>() + val courseScrapState: LiveData> get() = _courseScrapState private val _multiViewItems: MutableList> = mutableListOf() diff --git a/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileActivity.kt index 361bf01dd..21d30c6d7 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileActivity.kt @@ -1,27 +1,142 @@ package com.runnect.runnect.presentation.profile +import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import androidx.core.view.isVisible import com.runnect.runnect.R import com.runnect.runnect.binding.BindingActivity import com.runnect.runnect.databinding.ActivityProfileBinding +import com.runnect.runnect.presentation.detail.CourseDetailActivity +import com.runnect.runnect.presentation.state.UiStateV2 +import com.runnect.runnect.util.extension.applyScreenEnterAnimation +import com.runnect.runnect.util.extension.showSnackbar import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class ProfileActivity : BindingActivity(R.layout.activity_profile) { private val viewModel: ProfileViewModel by viewModels() private lateinit var adapter: ProfileCourseAdapter + private var userId: Int = -1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding.vm = viewModel binding.lifecycleOwner = this initAdapter() + addListener() + addObserver() + getIntentExtra() + getUserProfile() + } + + private fun getIntentExtra() { + userId = intent.getIntExtra(EXTRA_COURSE_USER_ID, -1) } private fun initAdapter() { - adapter = ProfileCourseAdapter().also { adapter -> + adapter = ProfileCourseAdapter(onScrapButtonClick = { courseId, scrapTF -> + viewModel.postCourseScrap(courseId = courseId, scrapTF = scrapTF) + }, onCourseItemClick = { courseId -> + navigateToCourseDetail(courseId) + }).also { adapter -> binding.rvProfileUploadCourse.adapter = adapter - adapter.submitList(viewModel.courseList) } } + + private fun addListener() { + initBackButtonClickListener() + } + + private fun addObserver() { + setupUserProfileGetStateObserver() + setupCourseScrapPostStateObserver() + } + + private fun navigateToCourseDetail(courseId: Int) { + Intent(this@ProfileActivity, CourseDetailActivity::class.java).apply { + putExtra(EXTRA_PUBLIC_COURSE_ID, courseId) + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + startActivity(this) + } + applyScreenEnterAnimation() + } + + private fun initBackButtonClickListener() { + binding.ivProfileBack.setOnClickListener { + finish() + } + } + + private fun getUserProfile() { + viewModel.getUserProfile(userId = userId) + } + + private fun setupUserProfileGetStateObserver() { + viewModel.userProfileState.observe(this) { state -> + when (state) { + is UiStateV2.Loading -> { + activateLoadingProgressBar() + } + + is UiStateV2.Success -> { + deactivateLoadingProgressBar() + binding.data = state.data + adapter.submitList(state.data.courseData) + } + + is UiStateV2.Failure -> { + deactivateLoadingProgressBar() + this.showSnackbar(binding.root, state.msg) + } + + else -> { + + } + } + + } + } + + private fun setupCourseScrapPostStateObserver() { + viewModel.courseScrapState.observe(this) { state -> + when (state) { + is UiStateV2.Loading -> { + + } + + is UiStateV2.Success -> { + state.data?.let { it -> + adapter.updateCourseItem( + courseId = it.publicCourseId.toInt(), + scrapTF = it.scrapTF + ) + } + } + + is UiStateV2.Failure -> { + this.showSnackbar(binding.root, state.msg) + } + + else -> { + + } + } + + } + } + + private fun activateLoadingProgressBar() { + binding.clProfile.isVisible = false + binding.pbProfileIntermediate.isVisible = true + } + + private fun deactivateLoadingProgressBar() { + binding.clProfile.isVisible = true + binding.pbProfileIntermediate.isVisible = false + } + + companion object { + private const val EXTRA_COURSE_USER_ID = "courseUserId" + private const val EXTRA_PUBLIC_COURSE_ID = "publicCourseId" + } } \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileCourseAdapter.kt b/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileCourseAdapter.kt index 11ddf62e4..8d4966a37 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileCourseAdapter.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileCourseAdapter.kt @@ -4,12 +4,15 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.runnect.runnect.data.dto.ProfileCourseData +import com.runnect.runnect.domain.entity.UserCourse import com.runnect.runnect.databinding.ItemProfileCourseBinding import com.runnect.runnect.util.callback.diff.ItemDiffCallback +import com.runnect.runnect.util.extension.setOnSingleClickListener class ProfileCourseAdapter( -) : ListAdapter(diffUtil) { + private val onScrapButtonClick: (Int, Boolean) -> Unit, + private val onCourseItemClick: (Int) -> Unit +) : ListAdapter(diffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UploadedCourseViewHolder { return UploadedCourseViewHolder( @@ -17,7 +20,9 @@ class ProfileCourseAdapter( LayoutInflater.from(parent.context), parent, false - ) + ), + onScrapButtonClick = onScrapButtonClick, + onCourseItemClick = onCourseItemClick ) } @@ -26,17 +31,35 @@ class ProfileCourseAdapter( } class UploadedCourseViewHolder( - private val binding: ItemProfileCourseBinding + private val binding: ItemProfileCourseBinding, + private val onScrapButtonClick: (Int, Boolean) -> Unit, + private val onCourseItemClick: (Int) -> Unit ) : RecyclerView.ViewHolder(binding.root) { - fun bind(profileCourseData: ProfileCourseData) { + fun bind(userCourse: UserCourse) { with(binding) { - data = profileCourseData + data = userCourse + ivItemProfileCourseHeart.setOnSingleClickListener { + onScrapButtonClick(userCourse.publicCourseId, !userCourse.scrapTF) + } + + clItemProfileCourse.setOnSingleClickListener { + onCourseItemClick(userCourse.publicCourseId) + } + } + } + } + + fun updateCourseItem(courseId: Int, scrapTF: Boolean) { + currentList.forEachIndexed { index, userCourseData -> + if (userCourseData.publicCourseId == courseId) { + userCourseData.scrapTF = scrapTF + notifyItemChanged(index) } } } companion object { - private val diffUtil = ItemDiffCallback( + private val diffUtil = ItemDiffCallback( onItemsTheSame = { old, new -> old.courseId == new.courseId }, onContentsTheSame = { old, new -> old == new } ) diff --git a/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileViewModel.kt b/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileViewModel.kt index e5667de47..b518cc418 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/profile/ProfileViewModel.kt @@ -1,40 +1,72 @@ package com.runnect.runnect.presentation.profile +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.runnect.runnect.data.dto.DepartureData -import com.runnect.runnect.data.dto.ProfileCourseData - -class ProfileViewModel : ViewModel() { - val courseList: List = generateMockData() - - private fun generateMockData(): List { - val mockDataList = mutableListOf() - - val mockData1 = ProfileCourseData( - publicCourseId = 1, - courseId = 101, - title = "제목 1", - image = "이미지 1", - departure = DepartureData( - region = "지역 1", - city = "도시 1", - town = "동네 1", - detail = null, - name = "출발지 1" - ), - scrapTF = true - ) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - mockDataList.add(mockData1) - return mockDataList +import androidx.lifecycle.viewModelScope +import com.runnect.runnect.data.dto.request.RequestPostCourseScrap +import com.runnect.runnect.data.dto.response.ResponsePostScrap +import com.runnect.runnect.domain.entity.UserProfile +import com.runnect.runnect.domain.repository.CourseRepository +import com.runnect.runnect.domain.repository.UserRepository +import com.runnect.runnect.presentation.state.UiStateV2 +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val userRepository: UserRepository, + private val courseRepository: CourseRepository +) : + ViewModel() { + + private val _courseScrapState = MutableLiveData>() + val courseScrapState: LiveData> + get() = _courseScrapState + + private val _userProfileState = MutableLiveData>() + val userProfileState: LiveData> + get() = _userProfileState + + + fun getUserProfile(userId: Int) { + viewModelScope.launch { + _userProfileState.value = UiStateV2.Loading + + userRepository.getUserProfile(userId = userId) + .onSuccess { profileData -> + if (profileData == null) { + _userProfileState.value = UiStateV2.Failure("PROFILE DATA IS NULL") + Timber.d("PROFILE DATA IS NULL") + return@launch + } + _userProfileState.value = UiStateV2.Success(profileData) + Timber.d("GET PROFILE DATA SUCCESS") + } + .onFailure { error -> + _userProfileState.value = UiStateV2.Failure(error.message.toString()) + Timber.e("GET PROFILE DATA FAILURE") + } + } + } + + fun postCourseScrap(courseId: Int, scrapTF: Boolean) { + viewModelScope.launch { + _courseScrapState.value = UiStateV2.Loading + courseRepository.postCourseScrap( + RequestPostCourseScrap( + publicCourseId = courseId, scrapTF = scrapTF.toString() + ) + ).onSuccess { response -> + Timber.d("POST COURSE SCRAP SUCCESS") + _courseScrapState.value = UiStateV2.Success(response) + }.onFailure { exception -> + Timber.e("POST COURSE SCRAP FAILURE") + _courseScrapState.value = UiStateV2.Failure(exception.message.toString()) + } + } } } + diff --git a/app/src/main/java/com/runnect/runnect/util/binding/BindingAdapter.kt b/app/src/main/java/com/runnect/runnect/util/binding/BindingAdapter.kt index 6f60357be..782755995 100644 --- a/app/src/main/java/com/runnect/runnect/util/binding/BindingAdapter.kt +++ b/app/src/main/java/com/runnect/runnect/util/binding/BindingAdapter.kt @@ -12,6 +12,22 @@ fun ImageView.setLocalImageByResourceId(resId: Int) { load(resId) } +@BindingAdapter("setStampImageByResourceId") +fun ImageView.setStampImageByResourceId(stampId: String?) { + val resNameParam = "mypage_img_stamp_" + val resType = "drawable" + val packageName = context.packageName + + var resName = "" + resName = if (stampId == "CSPR0") { + "${resNameParam}basic" + } else { + "${resNameParam}$stampId" + } + val resId = context.resources.getIdentifier(resName, resType, packageName) + setImageResource(resId) +} + @BindingAdapter("setImageUrl") fun ImageView.setImageUrl(url: String?) { if (url == null) return diff --git a/app/src/main/res/layout/activity_course_detail.xml b/app/src/main/res/layout/activity_course_detail.xml index 98a48a8c1..32b5f29bb 100644 --- a/app/src/main/res/layout/activity_course_detail.xml +++ b/app/src/main/res/layout/activity_course_detail.xml @@ -178,7 +178,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="15dp" - android:layout_marginTop="14dp" + android:layout_marginTop="18dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_course_detail_title" app:srcCompat="@drawable/all_star" /> @@ -200,7 +200,7 @@ android:id="@+id/tv_course_detail_distance" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="34dp" + android:layout_marginStart="36dp" android:fontFamily="@font/pretendard_regular" android:text="@{courseDetail.distance}" android:textColor="@color/G1" @@ -227,7 +227,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="15dp" - android:layout_marginTop="8dp" + android:layout_marginTop="9dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/iv_course_detail_distance_indicator" app:srcCompat="@drawable/all_star" /> @@ -249,7 +249,7 @@ android:id="@+id/tv_course_detail_departure" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="23dp" + android:layout_marginStart="25dp" android:ellipsize="end" android:fontFamily="@font/pretendard_regular" android:maxLength="25" @@ -299,7 +299,7 @@ android:id="@+id/cl_course_detail_bottom_container" android:layout_width="0dp" android:layout_height="wrap_content" - android:paddingBottom="34dp" + android:layout_marginBottom="18dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> @@ -308,8 +308,8 @@ android:id="@+id/iv_course_detail_scrap" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="26dp" - android:layout_marginTop="13dp" + android:layout_marginStart="16dp" + android:layout_marginTop="17dp" android:layout_marginEnd="17dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -319,11 +319,11 @@ android:id="@+id/tv_course_detail_scrap_count" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="10dp" + android:layout_marginTop="2dp" android:fontFamily="@font/pretendard_semibold" android:text="@{courseDetail.scrapCount}" android:textColor="@color/G2" - android:textSize="11sp" + android:textSize="10sp" app:layout_constraintEnd_toEndOf="@id/iv_course_detail_scrap" app:layout_constraintStart_toStartOf="@id/iv_course_detail_scrap" app:layout_constraintTop_toBottomOf="@id/iv_course_detail_scrap" @@ -332,9 +332,10 @@ - + @@ -13,6 +15,7 @@ @@ -27,16 +30,20 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/M4" + app:contentInsetStart="0dp" app:layout_constraintTop_toTopOf="@id/cl_profile_toolbar"> + + setStampImageByResourceId="@{data.latestStamp}" /> + android:text="@{data.nickname}" /> - + android:text="@{Integer.toString(data.level)}"/> + android:progress="@{data.levelPercent}" /> + android:text="@{Integer.toString(data.levelPercent)}" /> + type="com.runnect.runnect.domain.entity.UserCourse" /> @@ -19,15 +20,15 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="3dp" android:layout_marginBottom="20dp" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:scale_base_height="154" app:scale_base_width="162">