Skip to content

Commit

Permalink
feat: 프로필 이미지 변경 및 타임라인 닉네임 표시 #538 (#603)
Browse files Browse the repository at this point in the history
* chore: 사용되지 않는 interface 파일 삭제

* refactor: PhotoAttachFragment의 uriSelectedListener가 초기화 되지 않았을 때 예외 구체화

* refactor: formDataName 정의를 위한 String 상수를 FileUtils.kt로 이동

* feat: 마이 페이지에서 프로필 이미지 변경 기능 구현

* refactor: MyPageMenuHandler -> MyPageHandler로 이름 변경

* feat: 프로필 이미지 변경 실패 메시지 추가 및 예외 처리

* ui: 메인 화면 프로필 사진 테두리 디자인 수정

* refactor: 마이페이지에서 네트워크 요청 대신 캐시 메모리로 멤버 프로필 로드

* feat: SharedViewModel에 Hilt 적용 및 멤버 프로필 로직 추가

* feat: UserInfoPreferencesManager에 프로필 사진, 닉네임, 복구 코드 관련 로직 추가

* feat: 메인 화면 진입 시 멤버 프로필 불러오는 로직 추가

* feat: 메인에서 마이페이지 이동 시 ActivityResultLauncher 적용

* ui: 타임라인 타이틀 사용자 닉네임 표시

* refactor: 불필요한 데이터 바인딩 변수 제거

* refactor: ApiResult의 제네릭 타입 제약 제거

* refactor: MyPageRemoteDataSource 추상화 추가

* refactor: MyPageRemoteDataSource 추상화 적용

* refactor: MyPageLocalDataSource 추상화 추가

* refactor: UserInfoPreferencesManager가 MyPageLocalDataSource를 구현하도록 수정

* refactor: MyPageLocalDataSource 추상화 적용

* refactor: SharedPreferencesModule 추가

* refactor: MyPageDefaultRepository에 MyPageLocalDataSource 추가

* refactor:
메인 화면, 마이페이지 화면에 MyPageDefaultRepository 적용

* refactor: 프로퍼티 이름에서 멤버 변수를 뜻하는 m 키워드 제거

* refactor: 닉네임, 복구코드 관련 메서드 반환 타입 non-nullable로 수정

* refactor: 바인딩 어댑터 formatNickname 가독성 개선

* style: 컨벤션에 맞게 메서드 순서 변경

* delete: 불필요한 drawableRes 파일 삭제

* ui: 프로필 사진 수정 버튼 터치 영역 확대

* refactor: MyPageActivity 메서드 순서 변경

* fix: StaccatoDetailResponse 응답값 매핑 오류 수정 #606 (#607)

* fix: 오타 수정

* refactor: 바인딩 변수 네이밍 변경 menuHandler -> myPageHandler

* refactor: EMPTY_STRING를 MemberProfile 내부로 이동

---------

Co-authored-by: linirini <[email protected]>
  • Loading branch information
s6m1n and linirini authored Feb 6, 2025
1 parent 7f58bc0 commit f3b1e15
Show file tree
Hide file tree
Showing 31 changed files with 393 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ package com.on.staccato.data

import com.on.staccato.data.dto.Status

sealed interface ApiResult<T : Any>
sealed interface ApiResult<T>

class Success<T : Any>(val data: T) : ApiResult<T>
class Success<T>(val data: T) : ApiResult<T>

class ServerError<T : Any>(val status: Status, val message: String) : ApiResult<T>
class ServerError<T>(val status: Status, val message: String) : ApiResult<T>

sealed class Exception<T : Any> : ApiResult<T> {
class NetworkError<T : Any> : Exception<T>()
sealed class Exception<T> : ApiResult<T> {
class NetworkError<T> : Exception<T>()

class UnknownError<T : Any> : Exception<T>()
class UnknownError<T> : Exception<T>()
}

inline fun <T : Any, R : Any> ApiResult<T>.handle(convert: (T) -> R): ApiResult<R> =
inline fun <T, R> ApiResult<T>.handle(convert: (T) -> R): ApiResult<R> =
when (this) {
is Exception.NetworkError -> Exception.NetworkError()
is Exception.UnknownError -> Exception.UnknownError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ package com.on.staccato.data

import com.on.staccato.presentation.util.ExceptionState

inline fun <T : Any> ApiResult<T>.onSuccess(action: (T) -> Unit): ApiResult<T> =
inline fun <T> ApiResult<T>.onSuccess(action: (T) -> Unit): ApiResult<T> =
apply {
if (this is Success<T>) action(data)
}

inline fun <T : Any> ApiResult<T>.onServerError(action: (message: String) -> Unit): ApiResult<T> =
inline fun <T> ApiResult<T>.onServerError(action: (message: String) -> Unit): ApiResult<T> =
apply {
if (this is ServerError<T>) action(message)
}

inline fun <T : Any> ApiResult<T>.onException(action: (exceptionState: ExceptionState) -> Unit): ApiResult<T> =
inline fun <T> ApiResult<T>.onException(action: (exceptionState: ExceptionState) -> Unit): ApiResult<T> =
apply {
if (this is Exception<T>) {
when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,82 @@ package com.on.staccato.data

import android.content.Context
import android.content.SharedPreferences
import com.on.staccato.data.mypage.MyPageLocalDataSource
import com.on.staccato.domain.model.MemberProfile
import com.on.staccato.domain.model.MemberProfile.Companion.EMPTY_STRING
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class UserInfoPreferencesManager(context: Context) {
private val mUserInfoPrefs: SharedPreferences =
class UserInfoPreferencesManager(context: Context) : MyPageLocalDataSource {
private val userInfoPrefs: SharedPreferences =
context.getSharedPreferences(USER_INFO_PREF_NAME, Context.MODE_PRIVATE)

suspend fun getToken(): String? {
return withContext(Dispatchers.IO) {
mUserInfoPrefs.getString(TOKEN_KEY_NAME, "")
userInfoPrefs.getString(TOKEN_KEY_NAME, EMPTY_STRING)
}
}

override suspend fun getMemberProfile(): MemberProfile =
MemberProfile(
profileImageUrl = getProfileImageUrl(),
nickname = getNickname(),
uuidCode = getRecoveryCode(),
)

private suspend fun getProfileImageUrl(): String? {
return withContext(Dispatchers.IO) {
userInfoPrefs.getString(PROFILE_IMAGE_URL_KEY_NAME, null)
}
}

private suspend fun getNickname(): String {
return withContext(Dispatchers.IO) {
userInfoPrefs.getString(NICKNAME_KEY_NAME, null) ?: EMPTY_STRING
}
}

private suspend fun getRecoveryCode(): String {
return withContext(Dispatchers.IO) {
userInfoPrefs.getString(RECOVERY_CODE_KEY_NAME, null) ?: EMPTY_STRING
}
}

suspend fun setToken(newToken: String) {
withContext(Dispatchers.IO) {
mUserInfoPrefs.edit().putString(TOKEN_KEY_NAME, newToken).apply()
userInfoPrefs.edit().putString(TOKEN_KEY_NAME, newToken).apply()
}
}

override suspend fun updateMemberProfile(memberProfile: MemberProfile) {
updateProfileImageUrl(memberProfile.profileImageUrl)
updateNickname(memberProfile.nickname)
updateRecoveryCode(memberProfile.uuidCode)
}

override suspend fun updateProfileImageUrl(url: String?) {
withContext(Dispatchers.IO) {
userInfoPrefs.edit().putString(PROFILE_IMAGE_URL_KEY_NAME, url ?: EMPTY_STRING).apply()
}
}

private suspend fun updateNickname(nickname: String) {
withContext(Dispatchers.IO) {
userInfoPrefs.edit().putString(NICKNAME_KEY_NAME, nickname).apply()
}
}

private suspend fun updateRecoveryCode(code: String) {
withContext(Dispatchers.IO) {
userInfoPrefs.edit().putString(RECOVERY_CODE_KEY_NAME, code).apply()
}
}

companion object {
private const val USER_INFO_PREF_NAME = "com.on.staccato.user_info_prefs"
private const val TOKEN_KEY_NAME = "com.on.staccato.token"
private const val PROFILE_IMAGE_URL_KEY_NAME = "com.on.staccato.profile"
private const val NICKNAME_KEY_NAME = "com.on.staccato.nickname"
private const val RECOVERY_CODE_KEY_NAME = "com.on.staccato.recovery"
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.on.staccato.data.module

import com.on.staccato.data.UserInfoPreferencesManager
import com.on.staccato.data.comment.CommentDataSource
import com.on.staccato.data.comment.CommentRemoteDataSource
import com.on.staccato.data.login.LoginDataSource
import com.on.staccato.data.login.LoginRemoteDataSource
import com.on.staccato.data.memory.MemoryDataSource
import com.on.staccato.data.memory.MemoryRemoteDataSource
import com.on.staccato.data.mypage.MyPageLocalDataSource
import com.on.staccato.data.mypage.MyPageRemoteDataSource
import com.on.staccato.data.mypage.MyPageRemoteDataSourceImpl
import com.on.staccato.data.staccato.StaccatoDataSource
import com.on.staccato.data.staccato.StaccatoRemoteDataSource
import com.on.staccato.data.timeline.TimelineDataSource
Expand All @@ -32,4 +36,10 @@ abstract class DataSourceModule {

@Binds
abstract fun bindTimelineDataSource(timelineRemoteDataSource: TimelineRemoteDataSource): TimelineDataSource

@Binds
abstract fun bindMyPageLocalDataSource(myPageLocalDataSource: UserInfoPreferencesManager): MyPageLocalDataSource

@Binds
abstract fun bindMyPageRemoteDataSource(myPageRemoteDataSource: MyPageRemoteDataSourceImpl): MyPageRemoteDataSource
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.on.staccato.data.module

import android.content.Context
import com.on.staccato.data.UserInfoPreferencesManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object SharedPreferencesModule {
@Singleton
@Provides
fun provideMemberProfileManager(
@ApplicationContext context: Context,
): UserInfoPreferencesManager = UserInfoPreferencesManager(context)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.on.staccato.data.mypage

import com.on.staccato.data.ApiResult
import com.on.staccato.data.Success
import com.on.staccato.data.dto.mapper.toDomain
import com.on.staccato.data.dto.mypage.ProfileImageResponse
import com.on.staccato.data.handle
import com.on.staccato.domain.model.MemberProfile
import com.on.staccato.domain.repository.MyPageRepository
Expand All @@ -12,14 +12,30 @@ import javax.inject.Inject
class MyPageDefaultRepository
@Inject
constructor(
private val myPageApiService: MyPageApiService,
private val myPageLocalDataSource: MyPageLocalDataSource,
private val myPageRemoteDataSource: MyPageRemoteDataSource,
) : MyPageRepository {
override suspend fun getMemberProfile(): ApiResult<MemberProfile> = myPageApiService.getMemberProfile().handle { it.toDomain() }
override suspend fun getMemberProfile(): ApiResult<MemberProfile> {
val localProfile = myPageLocalDataSource.getMemberProfile()
return if (localProfile.isValid()) {
Success(localProfile)
} else {
syncMemberProfile()
}
}

private suspend fun syncMemberProfile(): ApiResult<MemberProfile> {
return myPageRemoteDataSource.loadMemberProfile().handle {
val serverProfile = it.toDomain()
myPageLocalDataSource.updateMemberProfile(serverProfile)
serverProfile
}
}

override suspend fun changeProfileImage(profileImageFile: MultipartBody.Part): ApiResult<ProfileImageResponse> =
myPageApiService.postProfileImageChange(
profileImageFile,
).handle {
it
override suspend fun changeProfileImage(profileImageFile: MultipartBody.Part): ApiResult<String?> =
myPageRemoteDataSource.updateProfileImage(profileImageFile).handle {
val imageUrl = it.profileImageUrl
myPageLocalDataSource.updateProfileImageUrl(imageUrl)
imageUrl
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.on.staccato.data.mypage

import com.on.staccato.domain.model.MemberProfile

interface MyPageLocalDataSource {
suspend fun getMemberProfile(): MemberProfile

suspend fun updateMemberProfile(memberProfile: MemberProfile)

suspend fun updateProfileImageUrl(url: String?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.on.staccato.data.mypage

import com.on.staccato.data.ApiResult
import com.on.staccato.data.dto.mypage.MemberProfileResponse
import com.on.staccato.data.dto.mypage.ProfileImageResponse
import okhttp3.MultipartBody

interface MyPageRemoteDataSource {
suspend fun loadMemberProfile(): ApiResult<MemberProfileResponse>

suspend fun updateProfileImage(profileImageFile: MultipartBody.Part): ApiResult<ProfileImageResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.on.staccato.data.mypage

import com.on.staccato.data.ApiResult
import com.on.staccato.data.dto.mypage.MemberProfileResponse
import com.on.staccato.data.dto.mypage.ProfileImageResponse
import okhttp3.MultipartBody
import javax.inject.Inject

class MyPageRemoteDataSourceImpl
@Inject
constructor(
private val myPageApiService: MyPageApiService,
) : MyPageRemoteDataSource {
override suspend fun loadMemberProfile(): ApiResult<MemberProfileResponse> = myPageApiService.getMemberProfile()

override suspend fun updateProfileImage(profileImageFile: MultipartBody.Part): ApiResult<ProfileImageResponse> =
myPageApiService.postProfileImageChange(profileImageFile)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ data class MemberProfile(
val profileImageUrl: String? = null,
val nickname: String,
val uuidCode: String,
)
) {
fun isValid() = uuidCode.isNotEmpty() && nickname.isNotEmpty()

companion object {
const val EMPTY_STRING = ""
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.on.staccato.domain.repository

import com.on.staccato.data.ApiResult
import com.on.staccato.data.dto.mypage.ProfileImageResponse
import com.on.staccato.domain.model.MemberProfile
import okhttp3.MultipartBody

interface MyPageRepository {
suspend fun getMemberProfile(): ApiResult<MemberProfile>

suspend fun changeProfileImage(profileImageFile: MultipartBody.Part): ApiResult<ProfileImageResponse>
suspend fun changeProfileImage(profileImageFile: MultipartBody.Part): ApiResult<String?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ fun TextView.setIsMemoryCandidatesEmptyVisibility(memoryCandidates: MemoryCandid
isGone = !memoryCandidates?.memoryCandidate.isNullOrEmpty()
}

@BindingAdapter("timelineNickname")
fun TextView.formatTimelineNickname(nickname: String?) {
text = nickname?.let {
resources.getString(R.string.timeline_nickname_memories).formatNickname(it)
} ?: EMPTY_TEXT
}

fun String.formatNickname(nickname: String) = format(nickname.takeIf { it.length <= 10 } ?: ("${nickname.take(10)}..."))

@BindingAdapter("visitedAtHistory")
fun TextView.formatVisitedAtHistory(visitedAt: LocalDateTime?) {
text = visitedAt?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class PhotoAttachFragment : BottomSheetDialogFragment(), PhotoAttachHandler {
if (context is OnUrisSelectedListener) {
uriSelectedListener = context
} else {
throw RuntimeException()
throw IllegalStateException("Activity or Fragment must implement OnUrisSelectedListener")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class MainActivity :
private val locationPermissionManager =
LocationPermissionManager(context = this, activity = this)

private val myPageLauncher: ActivityResultLauncher<Intent> = handleMyPageResult()
val memoryCreationLauncher: ActivityResultLauncher<Intent> = handleMemoryResult()
val memoryUpdateLauncher: ActivityResultLauncher<Intent> = handleMemoryResult()
val staccatoCreationLauncher: ActivityResultLauncher<Intent> = handleStaccatoResult()
Expand All @@ -77,6 +78,8 @@ class MainActivity :
setupPermissionRequestLauncher()
setupGoogleMap()
setupFusedLocationProviderClient()
loadMemberProfile()
observeMemberProfile()
observeCurrentLocation()
observeStaccatoLocations()
observeStaccatoId()
Expand Down Expand Up @@ -114,8 +117,7 @@ class MainActivity :
}

override fun onMyPageClicked() {
val intent = Intent(this, MyPageActivity::class.java)
startActivity(intent)
MyPageActivity.startWithResultLauncher(this, myPageLauncher)
}

private fun setupPermissionRequestLauncher() {
Expand Down Expand Up @@ -192,6 +194,16 @@ class MainActivity :
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
}

private fun loadMemberProfile() {
sharedViewModel.fetchMemberProfile()
}

private fun observeMemberProfile() {
sharedViewModel.memberProfile.observe(this) { memberProfile ->
binding.memberProfile = memberProfile
}
}

private fun observeCurrentLocation() {
mapsViewModel.currentLocation.observe(this) { currentLocation ->
moveCurrentLocation(currentLocation)
Expand Down Expand Up @@ -347,6 +359,13 @@ class MainActivity :
}
}

private fun handleMyPageResult() =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
loadMemberProfile()
}
}

private fun makeBundle(
it: Intent,
keyName: String,
Expand Down
Loading

0 comments on commit f3b1e15

Please sign in to comment.