Skip to content

Commit 4695287

Browse files
authored
Merge pull request #30 from Noostak/feature/#29-edit-profile
[Feature/#29] : 프로필 수정 UI 구현
2 parents 273373e + 4174b18 commit 4695287

File tree

24 files changed

+530
-64
lines changed

24 files changed

+530
-64
lines changed

core/src/main/java/com/sopt/core/designsystem/component/image/ProfileImagePicker.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ fun ProfileImagePicker(
2828
) {
2929
Box(modifier = modifier) {
3030
GlideImage(
31-
imageModel = { selectedImageUri ?: R.drawable.ic_profile },
31+
imageModel = { selectedImageUri?.takeIf { it.isNotBlank() } ?: R.drawable.ic_profile },
3232
imageOptions = ImageOptions(
3333
contentScale = ContentScale.Crop,
3434
alignment = Alignment.Center

core/src/main/java/com/sopt/core/designsystem/component/textfield/NoostakTextField.kt

+28-3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ fun NoostakTextField(
5151
focusedBorderColor: Color = NoostakTheme.colors.blue600,
5252
unfocusedWithInputBorderColor: Color = NoostakTheme.colors.gray700,
5353
unfocusedBorderColor: Color = NoostakTheme.colors.gray500,
54+
errorBorderColor: Color = NoostakTheme.colors.red02,
5455
maxLength: Int = 30,
5556
modifier: Modifier = Modifier,
5657
visualTransformation: VisualTransformation = VisualTransformation.None,
@@ -61,6 +62,7 @@ fun NoostakTextField(
6162
var hasInvalidInput by remember { mutableStateOf(false) }
6263

6364
val validInputRegex = "^[a-zA-Z0-9가-힣]*$".toRegex()
65+
val isEmptyError = textFieldType == TextFieldType.EDITPROFILE && value.isBlank()
6466

6567
Column {
6668
Box(
@@ -69,7 +71,8 @@ fun NoostakTextField(
6971
.border(
7072
width = 1.dp,
7173
color = when {
72-
hasInvalidInput -> NoostakTheme.colors.red02
74+
isEmptyError -> errorBorderColor
75+
hasInvalidInput -> errorBorderColor
7376
isFocused -> focusedBorderColor // 포커스된 경우
7477
value.isNotEmpty() -> unfocusedWithInputBorderColor // 텍스트가 입력되고 포커스 안된 경우
7578
else -> unfocusedBorderColor // 포커스되지 않은 경우
@@ -84,7 +87,7 @@ fun NoostakTextField(
8487
value = value,
8588
textStyle = textStyle,
8689
onValueChange = { newValue ->
87-
if (textFieldType != TextFieldType.SIGNUP) {
90+
if (textFieldType != TextFieldType.SIGNUP && textFieldType != TextFieldType.EDITPROFILE) {
8891
if (newValue.replace(" ", "").length <= maxLength) {
8992
onValueChange(newValue)
9093
}
@@ -148,14 +151,36 @@ fun NoostakTextField(
148151
) {
149152
if (textFieldType == TextFieldType.SIGNUP && hasInvalidInput) {
150153
Text(
151-
text = stringResource(R.string.text_noostak_text_field_sign_up_condition),
154+
text = stringResource(R.string.text_noostak_text_field_condition),
152155
color = NoostakTheme.colors.red02,
153156
style = NoostakTheme.typography.c3SemiBold,
154157
modifier = modifier.padding(top = 6.dp),
155158
maxLines = 1
156159
)
157160
}
158161

162+
if (textFieldType == TextFieldType.EDITPROFILE) {
163+
when {
164+
value.isBlank() ->
165+
Text(
166+
text = stringResource(R.string.text_noostak_text_field_edit_profile_empty),
167+
color = NoostakTheme.colors.red02,
168+
style = NoostakTheme.typography.c3SemiBold,
169+
modifier = modifier.padding(top = 6.dp),
170+
maxLines = 1
171+
)
172+
173+
hasInvalidInput ->
174+
Text(
175+
text = stringResource(R.string.text_noostak_text_field_condition),
176+
color = NoostakTheme.colors.red02,
177+
style = NoostakTheme.typography.c3SemiBold,
178+
modifier = modifier.padding(top = 6.dp),
179+
maxLines = 1
180+
)
181+
}
182+
}
183+
159184
Spacer(modifier = Modifier.weight(1f))
160185
Text(
161186
text = stringResource(

core/src/main/java/com/sopt/core/type/TextFieldType.kt

+3
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ enum class TextFieldType(
1414
),
1515
CALENDAR(
1616
placeholder = R.string.tf_calendar_info_placeholder
17+
),
18+
EDITPROFILE(
19+
placeholder = R.string.tf_edit_profile_placeholder
1720
)
1821
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.sopt.core.util.permission
2+
3+
import android.Manifest
4+
import android.os.Build
5+
import androidx.activity.compose.rememberLauncherForActivityResult
6+
import androidx.activity.result.contract.ActivityResultContracts
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.LaunchedEffect
9+
import timber.log.Timber
10+
11+
@Composable
12+
fun RequestGalleryPermission(onPermissionResult: (Boolean) -> Unit) {
13+
val permissionLauncher = rememberLauncherForActivityResult(
14+
contract = ActivityResultContracts.RequestPermission()
15+
) { isGranted ->
16+
try {
17+
onPermissionResult(isGranted)
18+
} catch (e: Exception) {
19+
Timber.e(e)
20+
}
21+
}
22+
23+
LaunchedEffect(Unit) {
24+
val permission = when {
25+
Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU -> Manifest.permission.READ_MEDIA_IMAGES
26+
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU -> Manifest.permission.READ_EXTERNAL_STORAGE
27+
else -> return@LaunchedEffect
28+
}
29+
permissionLauncher.launch(permission)
30+
}
31+
}

core/src/main/res/values/strings.xml

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
<!-- Calendar Info -->
1919
<string name="tf_calendar_info_placeholder">이름을 입력해주세요</string>
2020

21+
<!-- Edit Profile -->
22+
<string name="tf_edit_profile_placeholder">이름을 입력해주세요</string>
23+
2124
<!-- Noostak Text Field -->
22-
<string name="text_noostak_text_field_sign_up_condition">띄어쓰기 없이 한글, 영문, 숫자만 가능해요.</string>
25+
<string name="text_noostak_text_field_condition">띄어쓰기 없이 한글, 영문, 숫자만 가능해요.</string>
2326
<string name="text_noostak_text_field_count">%1$s/%2$s</string>
2427
<string name="icon_noostak_text_field_descrition">Delete Profile Name</string>
28+
<string name="text_noostak_text_field_edit_profile_empty">이름을 입력해주세요!</string>
2529

2630
<!-- Profile Image Picker -->
2731
<string name="image_profile_image_picker_description">Add Profile Image By Camera Button</string>

data/src/main/java/com/sopt/data/datasource/UserDataSource.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface UserDataSource {
77
val refreshToken: Flow<String>
88
val userId: Flow<Int>
99
val isAutoLogin: Flow<Boolean>
10-
val nickName: Flow<String>
10+
val nickname: Flow<String>
1111
val profileImage: Flow<String>
1212

1313
suspend fun updateAccessToken(accessToken: String)
@@ -18,7 +18,7 @@ interface UserDataSource {
1818

1919
suspend fun updateIsAutoLogin(isAutoLogin: Boolean)
2020

21-
suspend fun updateNickName(nickName: String)
21+
suspend fun updateNickname(nickname: String)
2222

2323
suspend fun updateProfileImage(profileImage: String)
2424

data/src/main/java/com/sopt/data/datasourceimpl/UserDataSourceImpl.kt

+5-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class UserDataSourceImpl @Inject constructor(
2323
val refreshToken = stringPreferencesKey("refreshToken")
2424
val userId = intPreferencesKey("userId")
2525
val isAutoLogin = booleanPreferencesKey("isAutoLogin")
26-
val nickName = stringPreferencesKey("nickName")
26+
val nickname = stringPreferencesKey("nickname")
2727
val profileImage = stringPreferencesKey("profileImage")
2828
}
2929

@@ -51,10 +51,10 @@ class UserDataSourceImpl @Inject constructor(
5151
preferences[PreferencesKeys.isAutoLogin] ?: false
5252
}
5353

54-
override val nickName: Flow<String> = dataStore.data
54+
override val nickname: Flow<String> = dataStore.data
5555
.catch { handleError(it) }
5656
.map { preferences ->
57-
preferences[PreferencesKeys.nickName].orEmpty()
57+
preferences[PreferencesKeys.nickname].orEmpty()
5858
}
5959

6060
override var profileImage: Flow<String> = dataStore.data
@@ -87,9 +87,9 @@ class UserDataSourceImpl @Inject constructor(
8787
}
8888
}
8989

90-
override suspend fun updateNickName(nickName: String) {
90+
override suspend fun updateNickname(nickname: String) {
9191
dataStore.edit { preferences ->
92-
preferences[PreferencesKeys.nickName] = nickName
92+
preferences[PreferencesKeys.nickname] = nickname
9393
}
9494
}
9595

data/src/main/java/com/sopt/data/repositoryimpl/UserInfoRepositoryImpl.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class UserInfoRepositoryImpl @Inject constructor(
1616

1717
override fun getIsAutoLogin(): Flow<Boolean> = authDataSource.isAutoLogin
1818

19-
override fun getNickName(): Flow<String> = authDataSource.nickName
19+
override fun getNickname(): Flow<String> = authDataSource.nickname
2020

2121
override fun getProfileImage(): Flow<String> = authDataSource.profileImage
2222

@@ -36,8 +36,8 @@ class UserInfoRepositoryImpl @Inject constructor(
3636
authDataSource.updateIsAutoLogin(isAutoLogin)
3737
}
3838

39-
override suspend fun saveNickName(nickName: String) {
40-
authDataSource.updateNickName(nickName)
39+
override suspend fun saveNickname(nickname: String) {
40+
authDataSource.updateNickname(nickname)
4141
}
4242

4343
override suspend fun saveProfileImage(profileImage: String) {
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.sopt.domain.entity
22

33
data class UserEntity(
4-
val accessToken: String?,
5-
val refreshToken: String?,
6-
val userId: Int?,
7-
val isAutoLogin: Boolean,
8-
val nickName: String,
9-
val profileImage: String?
4+
val accessToken: String? = null,
5+
val refreshToken: String? = null,
6+
val userId: Int? = null,
7+
val isAutoLogin: Boolean = false,
8+
val nickname: String = "",
9+
val profileImage: String? = null
1010
)

domain/src/main/java/com/sopt/domain/repository/UserInfoRepository.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ interface UserInfoRepository {
77
fun getRefreshToken(): Flow<String>
88
fun getUserId(): Flow<Int>
99
fun getIsAutoLogin(): Flow<Boolean>
10-
fun getNickName(): Flow<String>
10+
fun getNickname(): Flow<String>
1111
fun getProfileImage(): Flow<String>
1212

1313
suspend fun saveAccessToken(accessToken: String)
1414
suspend fun saveRefreshToken(refreshToken: String)
1515
suspend fun saveUserId(userId: Int)
1616
suspend fun saveIsAutoLogin(isAutoLogin: Boolean)
17-
suspend fun saveNickName(nickName: String)
17+
suspend fun saveNickname(nickname: String)
1818
suspend fun saveProfileImage(profileImage: String)
1919

2020
suspend fun clearAll()

presentation/src/main/java/com/sopt/presentation/auth/signup/SignUpRoute.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ fun SignUpRoute(
9898
onProfileSettingBtnClick = {
9999
handleProfileBtnClick(viewModel, permissionLauncher)
100100
},
101-
onNameChange = { viewModel.onUserNameChanged(it) },
101+
onNameChange = { viewModel.onNicknameChanged(it) },
102102
onSignUpClick = { viewModel.navigateToCheckInvite() }
103103
)
104104
}
@@ -148,14 +148,14 @@ fun SignUpScreen(
148148
Spacer(modifier = Modifier.height(27.dp))
149149
NoostakTextField(
150150
textFieldType = TextFieldType.SIGNUP,
151-
value = signUpState.userName,
151+
value = signUpState.nickname,
152152
maxLength = 10,
153153
onValueChange = onNameChange
154154
)
155155
Spacer(modifier = Modifier.weight(1f))
156156
NoostakBottomButton(
157157
text = stringResource(R.string.btn_next),
158-
isEnabled = signUpState.userName.isNotEmpty(),
158+
isEnabled = signUpState.nickname.isNotEmpty(),
159159
onButtonClick = onSignUpClick
160160
)
161161
}
@@ -167,7 +167,7 @@ fun SignUpScreenPreview() {
167167
NoostakAndroidTheme {
168168
SignUpScreen(
169169
signUpState = SignUpState(
170-
userName = stringResource(R.string.app_name),
170+
nickname = stringResource(R.string.app_name),
171171
profileImageUri = null
172172
),
173173
onProfileSettingBtnClick = {},

presentation/src/main/java/com/sopt/presentation/auth/signup/SignUpState.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.sopt.presentation.auth.signup
22

33
data class SignUpState(
4-
val userName: String = "",
4+
val nickname: String = "",
55
val profileImageUri: String? = null,
66
val authId: String = "",
77
val isPermissionGranted: Boolean = false,

presentation/src/main/java/com/sopt/presentation/auth/signup/SignUpViewModel.kt

+11-11
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ class SignUpViewModel @Inject constructor(
1919
private val _signUpState: MutableStateFlow<SignUpState> = MutableStateFlow(SignUpState())
2020
val signUpState: StateFlow<SignUpState> get() = _signUpState.asStateFlow()
2121

22-
fun onUserNameChanged(userName: String) {
23-
_signUpState.update { it.copy(userName = userName) }
24-
validateUserName(userName)
22+
fun onNicknameChanged(nickname: String) {
23+
_signUpState.update { it.copy(nickname = nickname) }
24+
validateNickname(nickname)
2525
}
2626

27-
private fun validateUserName(userName: String) {
28-
_signUpState.update { it.copy(isNameCheck = userName.length in 1..10) }
27+
private fun validateNickname(nickname: String) {
28+
_signUpState.update { it.copy(isNameCheck = nickname.length in 1..10) }
2929
}
3030

3131
fun updateGalleryPermissionState(isGranted: Boolean) {
@@ -52,16 +52,16 @@ class SignUpViewModel @Inject constructor(
5252
}
5353

5454
fun navigateToCheckInvite() {
55-
val name = _signUpState.value.userName
56-
if (name.isNotEmpty()) {
57-
saveUserNickName(name)
58-
emitSideEffect(SignUpSideEffect.NavigateToCheckInvite(name))
55+
val nickname = _signUpState.value.nickname
56+
if (nickname.isNotEmpty()) {
57+
saveUserNickname(nickname)
58+
emitSideEffect(SignUpSideEffect.NavigateToCheckInvite(nickname))
5959
}
6060
}
6161

62-
private fun saveUserNickName(nickName: String) {
62+
private fun saveUserNickname(nickname: String) {
6363
executeInScope {
64-
userInfoRepository.saveNickName(nickName)
64+
userInfoRepository.saveNickname(nickname)
6565
}
6666
}
6767

presentation/src/main/java/com/sopt/presentation/group/navigation/GroupNavigation.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptions
66
import androidx.navigation.compose.composable
77
import com.sopt.core.navigation.MainTabRoute
8+
import com.sopt.presentation.auth.signup.inputGroupCode.navigation.navigateInputGroupCode
89
import com.sopt.presentation.group.GroupRoute
910
import com.sopt.presentation.groupCreate.navigation.navigateToGroupCreate
10-
import com.sopt.presentation.groupCreate.navigation.navigateToGroupEnter
1111
import com.sopt.presentation.groupDetail.navigation.navigateGroupDetail
1212
import kotlinx.serialization.Serializable
1313

@@ -27,7 +27,7 @@ fun NavGraphBuilder.groupNavGraph(
2727
navHostController.navigateGroupDetail(groupId = groupId)
2828
},
2929
navigateToGroupCreate = { navHostController.navigateToGroupCreate() },
30-
navigateToGroupEnter = { navHostController.navigateToGroupEnter() }
30+
navigateToGroupEnter = { navHostController.navigateInputGroupCode() }
3131
)
3232
}
3333
}

presentation/src/main/java/com/sopt/presentation/groupCreate/navigation/GroupCreateNavigation.kt

-14
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ fun NavController.navigateToGroupCreate(navOptions: NavOptions? = null) {
1717
)
1818
}
1919

20-
fun NavController.navigateToGroupEnter(navOptions: NavOptions? = null) {
21-
navigate(
22-
route = GroupEnter,
23-
navOptions = navOptions
24-
)
25-
}
26-
2720
fun NavGraphBuilder.groupCreateNavGraph(
2821
paddingValues: PaddingValues,
2922
navHostController: NavController
@@ -36,14 +29,7 @@ fun NavGraphBuilder.groupCreateNavGraph(
3629
}
3730
)
3831
}
39-
40-
composable<GroupEnter> {
41-
// 이동 코드 추가하기
42-
}
4332
}
4433

4534
@Serializable
4635
data object GroupCreate : Route
47-
48-
@Serializable
49-
data object GroupEnter : Route

0 commit comments

Comments
 (0)