Skip to content

Commit

Permalink
Feat/#43 프로필 기능을 개발한다 (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhkrwngud445 authored Apr 1, 2024
1 parent 497802d commit b7087bb
Show file tree
Hide file tree
Showing 58 changed files with 1,790 additions and 20 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies {
implementation(project(":feature:mypage"))
implementation(project(":feature:registerpost"))
implementation(project(":feature:gallery"))
implementation(project(":feature:profileeditor"))
implementation(project(":core:interceptor"))
implementation(project(":core:data"))
implementation(project(":core:network"))
Expand Down
4 changes: 1 addition & 3 deletions app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.withpeace.withpeace
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
Expand Down Expand Up @@ -47,8 +46,7 @@ fun WithpeaceApp(
}
},
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding(),
.fillMaxSize(),
snackbarHost = { SnackbarHost(snackBarHostState) },
containerColor = WithpeaceTheme.colors.SystemWhite,
) { innerPadding ->
Expand Down
42 changes: 41 additions & 1 deletion app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.navOptions
import com.app.profileeditor.navigation.navigateProfileEditor
import com.app.profileeditor.navigation.profileEditorNavGraph
import com.withpeace.withpeace.feature.gallery.navigation.galleryNavGraph
import com.withpeace.withpeace.feature.gallery.navigation.navigateToGallery
import com.withpeace.withpeace.feature.home.navigation.homeNavGraph
import com.withpeace.withpeace.feature.login.navigation.LOGIN_ROUTE
import com.withpeace.withpeace.feature.login.navigation.loginNavGraph
import com.withpeace.withpeace.feature.login.navigation.navigateLogin
import com.withpeace.withpeace.feature.mypage.navigation.MY_PAGE_CHANGED_IMAGE_ARGUMENT
import com.withpeace.withpeace.feature.mypage.navigation.MY_PAGE_CHANGED_NICKNAME_ARGUMENT
import com.withpeace.withpeace.feature.mypage.navigation.myPageNavGraph
import com.withpeace.withpeace.feature.postlist.navigation.postListGraph
import com.withpeace.withpeace.feature.registerpost.navigation.IMAGE_LIST_ARGUMENT
Expand Down Expand Up @@ -54,7 +60,41 @@ fun WithpeaceNavHost(
onShowSnackBar = onShowSnackBar,
)
homeNavGraph(onShowSnackBar)
myPageNavGraph(
onShowSnackBar = onShowSnackBar,
onEditProfile = { nickname, profileImageUrl ->
navController.navigateProfileEditor(
nickname = nickname,
profileImageUrl = profileImageUrl,
)
},
onLogoutSuccess = {
navController.navigateLogin(
navOptions = navOptions {
popUpTo(navController.graph.id) {
inclusive = true
}
},
)
},
onWithdrawClick = {},
)
profileEditorNavGraph(
onShowSnackBar = onShowSnackBar,
onClickBackButton = {
navController.popBackStack()
},
onNavigateToGallery = {
navController.navigateToGallery(imageLimit = 1)
},
onUpdateSuccess = { nickname, imageUrl ->
navController.previousBackStackEntry?.savedStateHandle?.apply {
set(MY_PAGE_CHANGED_NICKNAME_ARGUMENT, nickname)
set(MY_PAGE_CHANGED_IMAGE_ARGUMENT, imageUrl)
}
navController.popBackStack()
},
)
postListGraph(onShowSnackBar)
myPageNavGraph(onShowSnackBar)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.withpeace.withpeace.core.data.di
import com.withpeace.withpeace.core.data.repository.DefaultImageRepository
import com.withpeace.withpeace.core.data.repository.DefaultPostRepository
import com.withpeace.withpeace.core.data.repository.DefaultTokenRepository
import com.withpeace.withpeace.core.data.repository.DefaultUserRepository
import com.withpeace.withpeace.core.domain.repository.ImageRepository
import com.withpeace.withpeace.core.domain.repository.PostRepository
import com.withpeace.withpeace.core.domain.repository.TokenRepository
import com.withpeace.withpeace.core.domain.repository.UserRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -27,4 +29,10 @@ interface RepositoryModule {
@Binds
@Singleton
fun bindsPostRepository(defaultPostRepository: DefaultPostRepository): PostRepository

@Binds
@Singleton
fun bindsUserRepository(
defaultUserRepository: DefaultUserRepository,
): UserRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.withpeace.withpeace.core.data.mapper

import com.withpeace.withpeace.core.domain.model.profile.ChangedProfile
import com.withpeace.withpeace.core.domain.model.profile.Nickname
import com.withpeace.withpeace.core.network.di.response.ChangedProfileResponse

fun ChangedProfileResponse.toDomain(): ChangedProfile {
return ChangedProfile(
nickname = Nickname(this.nickname),
profileImageUrl = profileImageUrl,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.withpeace.withpeace.core.data.mapper

import com.withpeace.withpeace.core.domain.model.profile.Nickname
import com.withpeace.withpeace.core.domain.model.profile.ProfileInfo
import com.withpeace.withpeace.core.network.di.response.ProfileResponse

fun ProfileResponse.toDomain(): ProfileInfo {
return ProfileInfo(
nickname = Nickname(nickname),
profileImageUrl = profileImageUrl,
email = email,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import javax.inject.Inject
class DefaultTokenRepository @Inject constructor(
private val tokenPreferenceDataSource: TokenPreferenceDataSource,
private val authService: AuthService,
private val userService: UserService,
) : TokenRepository {
override suspend fun isLogin(): Boolean {
val token = tokenPreferenceDataSource.accessToken.firstOrNull()
Expand All @@ -30,7 +29,7 @@ class DefaultTokenRepository @Inject constructor(
nickname: String,
onError: (String) -> Unit,
): Flow<Unit> = flow {
userService.signUp(
authService.signUp(
SignUpRequest(email = email, nickname = nickname, deviceToken = null),
).suspendMapSuccess {
val data = this.data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package com.withpeace.withpeace.core.data.repository

import android.content.Context
import android.net.Uri
import com.skydoves.sandwich.messageOrNull
import com.skydoves.sandwich.suspendMapSuccess
import com.skydoves.sandwich.suspendOnError
import com.skydoves.sandwich.suspendOnException
import com.withpeace.withpeace.core.data.mapper.toDomain
import com.withpeace.withpeace.core.data.util.convertToFile
import com.withpeace.withpeace.core.datastore.dataStore.TokenPreferenceDataSource
import com.withpeace.withpeace.core.domain.model.WithPeaceError
import com.withpeace.withpeace.core.domain.model.profile.ChangedProfile
import com.withpeace.withpeace.core.domain.model.profile.Nickname
import com.withpeace.withpeace.core.domain.model.profile.ProfileInfo
import com.withpeace.withpeace.core.domain.repository.UserRepository
import com.withpeace.withpeace.core.network.di.common.getErrorBody
import com.withpeace.withpeace.core.network.di.request.NicknameRequest
import com.withpeace.withpeace.core.network.di.service.UserService
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import javax.inject.Inject

class DefaultUserRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val userService: UserService,
private val tokenPreferenceDataSource: TokenPreferenceDataSource,
) : UserRepository {
override fun getProfile(
onError: suspend (WithPeaceError) -> Unit,
): Flow<ProfileInfo> = flow {
userService.getProfile().suspendMapSuccess {
emit(this.data.toDomain())
}.suspendOnError {
if (statusCode.code == 401) {
onError(WithPeaceError.UnAuthorized())
} else {
val errorBody = errorBody?.getErrorBody()
onError(WithPeaceError.GeneralError(errorBody?.code, errorBody?.message))
}
}.suspendOnException {
onError(WithPeaceError.GeneralError(message = messageOrNull))
}
}

override fun registerProfile(
nickname: String,
profileImage: String,
onError: (WithPeaceError) -> Unit,
): Flow<Unit> {
TODO("Not yet implemented")
}

override fun updateProfile(
nickname: String,
profileImage: String,
onError: suspend (WithPeaceError) -> Unit,
): Flow<ChangedProfile> = flow {
val imagePart = getImagePart(profileImage)
userService.updateProfile(
nickname.toRequestBody("text/plain".toMediaTypeOrNull()), imagePart,
).suspendMapSuccess {
emit(this.data.toDomain())
}.suspendOnError {
if (statusCode.code == 401) {
onError(WithPeaceError.UnAuthorized())
} else {
val errorBody = errorBody?.getErrorBody()
onError(WithPeaceError.GeneralError(errorBody?.code, errorBody?.message))
}
}.suspendOnException {
onError(WithPeaceError.GeneralError(message = messageOrNull))
}
}

override fun updateNickname(
nickname: String,
onError: suspend (WithPeaceError) -> Unit,
): Flow<ChangedProfile> =
flow {
userService.updateNickname(NicknameRequest(nickname)).suspendMapSuccess {
emit(ChangedProfile(nickname = Nickname(this.data)))
}.suspendOnError {
if (statusCode.code == 401) {
onError(WithPeaceError.UnAuthorized())
} else {
val errorBody = errorBody?.getErrorBody()
onError(WithPeaceError.GeneralError(errorBody?.code, errorBody?.message))
}
}.suspendOnException {
onError(WithPeaceError.GeneralError(message = messageOrNull))
}
}

override fun updateProfileImage(
profileImage: String,
onError: suspend (WithPeaceError) -> Unit,
): Flow<ChangedProfile> = flow {
val imagePart = getImagePart(profileImage)
userService.updateImage(imagePart).suspendMapSuccess {
emit(ChangedProfile(profileImageUrl = this.data))
}.suspendOnError {
if (statusCode.code == 401) {
onError(WithPeaceError.UnAuthorized())
} else {
val errorBody = errorBody?.getErrorBody()
onError(WithPeaceError.GeneralError(errorBody?.code, errorBody?.message))
}
}.suspendOnException {
onError(WithPeaceError.GeneralError(message = messageOrNull))
}
}

override fun verifyNicknameDuplicated(
nickname: Nickname,
onError: suspend (WithPeaceError) -> Unit,
): Flow<Unit> = flow {
userService.isNicknameDuplicate(nickname.value).suspendMapSuccess {
if (this.data) {
onError(WithPeaceError.GeneralError(code = 2))
} else {
emit(Unit)
}
}.suspendOnError {
onError(WithPeaceError.GeneralError(statusCode.code, messageOrNull))
}.suspendOnException {
onError(WithPeaceError.GeneralError(message = messageOrNull))
}
}

override fun logout(onError: suspend (WithPeaceError) -> Unit): Flow<Unit> = flow {
userService.logout().suspendMapSuccess {
tokenPreferenceDataSource.removeAll()
emit(Unit)
}.suspendOnError {
if (statusCode.code == 401) {
onError(WithPeaceError.UnAuthorized())
} else {
val errorBody = errorBody?.getErrorBody()
onError(WithPeaceError.GeneralError(errorBody?.code, errorBody?.message))
}
}.suspendOnException {
onError(WithPeaceError.GeneralError(message = messageOrNull))
}
}

private fun getImagePart(profileImage: String): MultipartBody.Part {
val requestFile: File = Uri.parse(profileImage).convertToFile(context)
val imageRequestBody = requestFile.asRequestBody("image/*".toMediaTypeOrNull())
return MultipartBody.Part.createFormData(
"imageFile",
requestFile.name,
imageRequestBody,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ class DefaultTokenPreferenceDataSource @Inject constructor(
}
}

override suspend fun removeAll() {
dataStore.edit { preferences ->
preferences.clear()
}
}

companion object {
private val ACCESS_TOKEN = stringPreferencesKey("ACCESS_TOKEN")
private val REFRESH_TOKEN = stringPreferencesKey("REFRESH_TOKEN")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ interface TokenPreferenceDataSource {
suspend fun updateRefreshToken(refreshToken: String)

suspend fun updateAccessToken(accessToken: String)
suspend fun removeAll()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.withpeace.withpeace.core.designsystem.ui

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme

@Composable
fun TitleBar(title: String, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.height(56.dp)
.padding(start = 24.dp),
) {
Text(
text = title,
style = WithpeaceTheme.typography.title1,
color = WithpeaceTheme.colors.SystemBlack,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.withpeace.withpeace.core.designsystem.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -41,6 +42,7 @@ fun WithPeaceBackButtonTopAppBar(
},
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(containerColor = WithpeaceTheme.colors.SystemWhite),
windowInsets = WindowInsets(0, 0, 0, 0),
)
}

Expand Down
Loading

0 comments on commit b7087bb

Please sign in to comment.