diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index db51f9120..cc12ba8ef 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,6 +23,9 @@ android:theme="@style/Theme.Naaga" android:usesCleartextTraffic="true" tools:targetApi="33"> + + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="false" + android:screenOrientation="portrait" /> diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/NicknameDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/NicknameDto.kt new file mode 100644 index 000000000..cbd176ee3 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/NicknameDto.kt @@ -0,0 +1,10 @@ +package com.now.naaga.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class NicknameDto( + @SerialName("nickname") + val nickname: String, +) diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/ProfileService.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/ProfileService.kt new file mode 100644 index 000000000..e9b82a30d --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/ProfileService.kt @@ -0,0 +1,18 @@ +package com.now.naaga.data.remote.retrofit.service + +import com.now.naaga.data.remote.dto.NicknameDto +import com.now.naaga.data.remote.dto.PlayerDto +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH + +interface ProfileService { + @GET("/profiles/my") + suspend fun getProfile(): Response + + @PATCH("/profiles/my") + suspend fun modifyNickname( + @Body nicknameDto: NicknameDto, + ): Response +} diff --git a/android/app/src/main/java/com/now/naaga/data/repository/DefaultProfileRepository.kt b/android/app/src/main/java/com/now/naaga/data/repository/DefaultProfileRepository.kt new file mode 100644 index 000000000..a2fe8165f --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/repository/DefaultProfileRepository.kt @@ -0,0 +1,23 @@ +package com.now.naaga.data.repository + +import com.now.domain.model.Player +import com.now.domain.repository.ProfileRepository +import com.now.naaga.data.mapper.toDomain +import com.now.naaga.data.remote.dto.NicknameDto +import com.now.naaga.data.remote.retrofit.service.ProfileService +import com.now.naaga.util.extension.getValueOrThrow + +class DefaultProfileRepository( + private val profileService: ProfileService, +) : ProfileRepository { + override suspend fun fetchProfile(): Player { + val response = profileService.getProfile().getValueOrThrow() + return response.toDomain() + } + + override suspend fun modifyNickname(nickname: String): String { + val nicknameDto = NicknameDto(nickname) + val response = profileService.modifyNickname(nicknameDto).getValueOrThrow() + return response.nickname + } +} diff --git a/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt b/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt index a05423df3..14f66f119 100644 --- a/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt +++ b/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt @@ -4,6 +4,7 @@ import com.now.domain.repository.AdventureRepository import com.now.domain.repository.AuthRepository import com.now.domain.repository.LetterRepository import com.now.domain.repository.PlaceRepository +import com.now.domain.repository.ProfileRepository import com.now.domain.repository.RankRepository import com.now.domain.repository.StatisticsRepository import com.now.naaga.data.local.AuthDataSource @@ -11,12 +12,14 @@ import com.now.naaga.data.remote.retrofit.service.AdventureService import com.now.naaga.data.remote.retrofit.service.AuthService import com.now.naaga.data.remote.retrofit.service.LetterService import com.now.naaga.data.remote.retrofit.service.PlaceService +import com.now.naaga.data.remote.retrofit.service.ProfileService import com.now.naaga.data.remote.retrofit.service.RankService import com.now.naaga.data.remote.retrofit.service.StatisticsService import com.now.naaga.data.repository.DefaultAdventureRepository import com.now.naaga.data.repository.DefaultAuthRepository import com.now.naaga.data.repository.DefaultLetterRepository import com.now.naaga.data.repository.DefaultPlaceRepository +import com.now.naaga.data.repository.DefaultProfileRepository import com.now.naaga.data.repository.DefaultRankRepository import com.now.naaga.data.repository.DefaultStatisticsRepository import dagger.Module @@ -55,4 +58,9 @@ class RepositoryModule { @Provides fun provideLetterRepository(letterService: LetterService): LetterRepository = DefaultLetterRepository(letterService) + + @Singleton + @Provides + fun provideProfileRepository(profileService: ProfileService): ProfileRepository = + DefaultProfileRepository(profileService) } diff --git a/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt b/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt index 340e12d02..0920fdb49 100644 --- a/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt +++ b/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt @@ -7,6 +7,7 @@ import com.now.naaga.data.remote.retrofit.service.AdventureService import com.now.naaga.data.remote.retrofit.service.AuthService import com.now.naaga.data.remote.retrofit.service.LetterService import com.now.naaga.data.remote.retrofit.service.PlaceService +import com.now.naaga.data.remote.retrofit.service.ProfileService import com.now.naaga.data.remote.retrofit.service.RankService import com.now.naaga.data.remote.retrofit.service.StatisticsService import dagger.Module @@ -61,4 +62,8 @@ class ServiceModule { @Singleton @Provides fun provideLetterService(retrofit: Retrofit): LetterService = retrofit.create(LetterService::class.java) + + @Singleton + @Provides + fun provideProfileService(retrofit: Retrofit): ProfileService = retrofit.create(ProfileService::class.java) } diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt index 516b77f63..1c50e185b 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt @@ -3,6 +3,7 @@ package com.now.naaga.presentation.mypage import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.now.domain.model.Statistics @@ -14,6 +15,7 @@ import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityMyPageBinding import com.now.naaga.presentation.adventurehistory.AdventureHistoryActivity import com.now.naaga.presentation.mypage.statistics.MyPageStatisticsAdapter +import com.now.naaga.presentation.profile.ProfileActivity import com.now.naaga.presentation.uimodel.model.StatisticsUiModel import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @@ -23,6 +25,14 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic private lateinit var binding: ActivityMyPageBinding private val viewModel: MyPageViewModel by viewModels() + private val myPageActivityLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + val nickname = it.data?.getStringExtra(NICKNAME_KEY) ?: "" + binding.tvMypageNickname.text = nickname + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMyPageBinding.inflate(layoutInflater) @@ -48,10 +58,14 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic val intent = AdventureHistoryActivity.getIntent(this) startActivity(intent) } + binding.ivMypageProfileModify.setOnClickListener { + val intent = ProfileActivity.getIntent(this) + myPageActivityLauncher.launch(intent) + } } private fun fetchData() { - viewModel.fetchRank() + viewModel.fetchProfile() viewModel.fetchStatistics() viewModel.fetchPlaces() } @@ -82,8 +96,16 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } companion object { + private const val NICKNAME_KEY = "nickname" + fun getIntent(context: Context): Intent { return Intent(context, MyPageActivity::class.java) } + + fun getIntent(context: Context, nickname: String): Intent { + return Intent(context, MyPageActivity::class.java).apply { + putExtra(NICKNAME_KEY, nickname) + } + } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt index 5a70439ea..e2f4122b4 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt @@ -5,12 +5,12 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.now.domain.model.Place -import com.now.domain.model.Rank +import com.now.domain.model.Player import com.now.domain.model.Statistics import com.now.domain.model.type.OrderType import com.now.domain.model.type.SortType import com.now.domain.repository.PlaceRepository -import com.now.domain.repository.RankRepository +import com.now.domain.repository.ProfileRepository import com.now.domain.repository.StatisticsRepository import com.now.naaga.data.throwable.DataThrowable import dagger.hilt.android.lifecycle.HiltViewModel @@ -20,12 +20,12 @@ import javax.inject.Inject @HiltViewModel class MyPageViewModel @Inject constructor( - private val rankRepository: RankRepository, + private val profileRepository: ProfileRepository, private val statisticsRepository: StatisticsRepository, private val placeRepository: PlaceRepository, ) : ViewModel() { - private val _rank = MutableLiveData() - val rank: LiveData = _rank + private val _profile = MutableLiveData() + val profile: LiveData = _profile private val _statistics = MutableLiveData() val statistics: LiveData = _statistics @@ -36,12 +36,12 @@ class MyPageViewModel @Inject constructor( private val _throwable = MutableLiveData() val throwable: LiveData = _throwable - fun fetchRank() { + fun fetchProfile() { viewModelScope.launch { runCatching { - rankRepository.getMyRank() - }.onSuccess { rank -> - _rank.value = rank + profileRepository.fetchProfile() + }.onSuccess { profile -> + _profile.value = profile }.onFailure { setThrowable(it) } diff --git a/android/app/src/main/java/com/now/naaga/presentation/profile/ProfileActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/profile/ProfileActivity.kt new file mode 100644 index 000000000..f7b780bd0 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/profile/ProfileActivity.kt @@ -0,0 +1,60 @@ +package com.now.naaga.presentation.profile + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.now.naaga.R +import com.now.naaga.databinding.ActivityProfileBinding +import com.now.naaga.presentation.mypage.MyPageActivity +import com.now.naaga.util.extension.showToast +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ProfileActivity : AppCompatActivity() { + private lateinit var binding: ActivityProfileBinding + private val viewModel: ProfileViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityProfileBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.lifecycleOwner = this + binding.viewModel = viewModel + subscribe() + setClickListeners() + } + + private fun subscribe() { + viewModel.modifyStatus.observe(this) { status -> + if (status) { + showToast(getString(R.string.profile_modify_success_message)) + setResult(RESULT_OK, MyPageActivity.getIntent(this, viewModel.nickname.value.toString())) + finish() + } + } + viewModel.throwable.observe(this) { + showToast(getString(R.string.profile_modify_fail_message)) + } + } + + private fun setClickListeners() { + binding.ivProfileBack.setOnClickListener { + finish() + } + binding.btnProfileNicknameModify.setOnClickListener { + if (viewModel.isFormValid()) { + viewModel.modifyNickname() + } else { + showToast(getString(R.string.profile_no_content_message)) + } + } + } + + companion object { + fun getIntent(context: Context): Intent { + return Intent(context, ProfileActivity::class.java) + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/profile/ProfileViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/profile/ProfileViewModel.kt new file mode 100644 index 000000000..3951c6877 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/profile/ProfileViewModel.kt @@ -0,0 +1,55 @@ +package com.now.naaga.presentation.profile + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.now.domain.repository.ProfileRepository +import com.now.naaga.data.throwable.DataThrowable +import com.now.naaga.data.throwable.DataThrowable.NetworkThrowable +import com.now.naaga.data.throwable.DataThrowable.PlayerThrowable +import com.now.naaga.data.throwable.DataThrowable.UniversalThrowable +import com.now.naaga.util.singleliveevent.MutableSingleLiveData +import com.now.naaga.util.singleliveevent.SingleLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import java.io.IOException +import javax.inject.Inject + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val profileRepository: ProfileRepository, +) : ViewModel() { + + val nickname = MutableLiveData() + + private val _modifyStatus = MutableSingleLiveData() + val modifyStatus: SingleLiveData = _modifyStatus + + private val _throwable = MutableLiveData() + val throwable: LiveData = _throwable + + fun modifyNickname() { + viewModelScope.launch { + runCatching { + profileRepository.modifyNickname(nickname.value.toString()) + }.onSuccess { + _modifyStatus.setValue(true) + }.onFailure { + setThrowable(it) + } + } + } + + fun isFormValid(): Boolean { + return nickname.value != null + } + + private fun setThrowable(throwable: Throwable) { + when (throwable) { + is IOException -> _throwable.value = NetworkThrowable() + is UniversalThrowable -> _throwable.value = throwable + is PlayerThrowable -> _throwable.value = throwable + } + } +} diff --git a/android/app/src/main/res/drawable/ic_profile_modify.xml b/android/app/src/main/res/drawable/ic_profile_modify.xml new file mode 100644 index 000000000..faddfce42 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_profile_modify.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/app/src/main/res/drawable/rect_border.xml b/android/app/src/main/res/drawable/rect_border.xml new file mode 100644 index 000000000..b90be845e --- /dev/null +++ b/android/app/src/main/res/drawable/rect_border.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/android/app/src/main/res/layout/activity_my_page.xml b/android/app/src/main/res/layout/activity_my_page.xml index 57ab97459..35663c5be 100644 --- a/android/app/src/main/res/layout/activity_my_page.xml +++ b/android/app/src/main/res/layout/activity_my_page.xml @@ -36,14 +36,12 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/space_default_large" - android:layout_marginBottom="@dimen/space_default_medium" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="1" - android:text="@{viewModel.rank.player.nickname}" + android:text="@{viewModel.profile.nickname}" android:textColor="@color/white" android:textSize="52sp" - app:layout_constraintBottom_toTopOf="@id/rv_mypage_statistics" app:layout_constraintEnd_toEndOf="@id/g_mypage" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/iv_mypage_back" @@ -53,25 +51,25 @@ android:id="@+id/btn_mypage_adventure_history" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="20dp" + android:layout_marginTop="@dimen/space_default_medium" + android:layout_marginHorizontal="24dp" android:paddingVertical="16dp" android:text="@string/mypage_adventure_record" - android:textSize="16sp" + android:textSize="24sp" app:radius="8dp" app:buttonColor="yellow" - app:layout_constraintBottom_toBottomOf="@id/tv_mypage_nickname" + app:layout_constraintTop_toBottomOf="@id/rv_mypage_statistics" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/g_mypage" - app:layout_constraintTop_toTopOf="@id/tv_mypage_nickname" /> + app:layout_constraintBottom_toTopOf="@id/customGrid_mypage_places" + app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintTop_toBottomOf="@id/btn_mypage_adventure_history" /> + + diff --git a/android/app/src/main/res/layout/activity_profile.xml b/android/app/src/main/res/layout/activity_profile.xml new file mode 100644 index 000000000..eaf8c79f3 --- /dev/null +++ b/android/app/src/main/res/layout/activity_profile.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_upload.xml b/android/app/src/main/res/layout/activity_upload.xml index 7058e7f08..337f7e5d8 100644 --- a/android/app/src/main/res/layout/activity_upload.xml +++ b/android/app/src/main/res/layout/activity_upload.xml @@ -106,14 +106,15 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/space_default_large" android:autofillHints="no" + android:backgroundTint="@color/light_gray" android:ellipsize="end" android:hint="@string/upload_photo_title_hint" - android:textColorHint="@color/white" android:inputType="text" android:maxLines="1" android:text="@={viewModel.name}" android:textAlignment="textEnd" android:textColor="@color/white" + android:textColorHint="@color/main_gray_opacity_medium" android:textSize="24sp" app:layout_constraintEnd_toEndOf="@id/v_upload_divide_line_1" app:layout_constraintStart_toStartOf="@id/v_upload_divide_line_1" diff --git a/android/app/src/main/res/layout/rv_mypage_item_adventure.xml b/android/app/src/main/res/layout/rv_mypage_item_adventure.xml index aebfee68b..acfc614e6 100644 --- a/android/app/src/main/res/layout/rv_mypage_item_adventure.xml +++ b/android/app/src/main/res/layout/rv_mypage_item_adventure.xml @@ -71,7 +71,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/space_default_medium" - android:layout_marginEnd="@dimen/space_default_large" + android:layout_marginEnd="32dp" android:text="@string/mypage_item_adventure_count_suffix" android:textColor="@color/white" android:textSize="20sp" diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index de53b92e0..5615c1307 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -152,6 +152,14 @@ 내용을 1자 이상 입력해주세요! 전송하기 + + 변경 할 닉네임을 입력하세요! + 한글 2~10 글자 사이\n영어 2~20 글자 사이\n공백 허용, 특수문자 사용 불가 + 닉네임 변경 + 변경 되었어요! + 변경을 실패 했어요! 다시 시도해주세요 + 변경 할 닉네임을 2글자 이상 작성해주세요! + 문제가 발생했어요. 다시 시도해주세요! diff --git a/android/domain/src/main/java/com/now/domain/repository/ProfileRepository.kt b/android/domain/src/main/java/com/now/domain/repository/ProfileRepository.kt new file mode 100644 index 000000000..d179c5df6 --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/repository/ProfileRepository.kt @@ -0,0 +1,9 @@ +package com.now.domain.repository + +import com.now.domain.model.Player + +interface ProfileRepository { + suspend fun fetchProfile(): Player + + suspend fun modifyNickname(nickname: String): String +}