diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8c455dc2..7a520aae 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/path_provider.xml b/app/src/main/res/xml/path_provider.xml
new file mode 100644
index 00000000..5b5e8874
--- /dev/null
+++ b/app/src/main/res/xml/path_provider.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieBottomSheetButton.kt b/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieBottomSheetButton.kt
new file mode 100644
index 00000000..e817b175
--- /dev/null
+++ b/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieBottomSheetButton.kt
@@ -0,0 +1,41 @@
+package com.whyranoid.presentation.component.button
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.whyranoid.presentation.theme.WalkieTypography
+
+@Composable
+fun WalkieBottomSheetButton(
+ modifier: Modifier = Modifier,
+ buttonText: String,
+ onClick: () -> Unit = {}
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(shape = RoundedCornerShape(10.dp))
+ .border(width = 1.dp, color = Color(0xFFE4E4E4), shape = RoundedCornerShape(10.dp))
+ .clickable { onClick() }
+ ) {
+ Text(
+ textAlign = TextAlign.Center,
+ text = buttonText,
+ style = WalkieTypography.Body1_Normal,
+ modifier = Modifier
+ .align(Alignment.Center)
+ .padding(vertical = 6.dp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieCircularIconButton.kt b/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieCircularIconButton.kt
new file mode 100644
index 00000000..1bd01c35
--- /dev/null
+++ b/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieCircularIconButton.kt
@@ -0,0 +1,44 @@
+package com.whyranoid.presentation.component.button
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun CircularIconButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ contentDescription: String? = null,
+ icon: ImageVector,
+ tint: Color = Color.White,
+ backgroundColor: Color = Color.Gray,
+ iconSize: Dp = 24.dp,
+ buttonSize: Dp = 48.dp,
+) {
+ Box(
+ modifier = modifier
+ .size(buttonSize)
+ .clip(CircleShape)
+ .background(backgroundColor)
+ .clickable(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = contentDescription,
+ tint = tint,
+ modifier = Modifier.size(iconSize),
+ )
+ }
+}
\ No newline at end of file
diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt
index e6226943..117d3e82 100644
--- a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt
+++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt
@@ -1,8 +1,12 @@
package com.whyranoid.presentation.screens.mypage.editprofile
+import android.Manifest
+import android.net.Uri
import android.widget.Toast
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
@@ -14,11 +18,18 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -37,48 +48,140 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.core.content.FileProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.whyranoid.domain.util.EMPTY
import com.whyranoid.presentation.R
+import com.whyranoid.presentation.component.button.CircularIconButton
+import com.whyranoid.presentation.component.button.WalkieBottomSheetButton
import com.whyranoid.presentation.component.button.WalkiePositiveButton
import com.whyranoid.presentation.reusable.WalkieTextField
import com.whyranoid.presentation.theme.WalkieTypography
+import com.whyranoid.presentation.util.createImageFile
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
+import java.util.Objects
+@OptIn(ExperimentalMaterialApi::class)
@Composable
fun EditProfileScreen(navController: NavController) {
val viewModel = koinViewModel()
val walkieId = viewModel.walkieId.collectAsStateWithLifecycle(initialValue = 0L)
val name = viewModel.name.collectAsStateWithLifecycle(initialValue = String.EMPTY)
val nick = viewModel.nick.collectAsStateWithLifecycle(initialValue = String.EMPTY)
- val profileImg = viewModel.profileImg.collectAsStateWithLifecycle(initialValue = String.EMPTY)
-
- EditProfileContent(
- walkieId = walkieId.value ?: 0L,
- name = name.value.orEmpty(),
- nick = nick.value.orEmpty(),
- profileImg = profileImg.value.orEmpty(),
- viewModel = viewModel,
+ var capturedUri by remember { mutableStateOf(Uri.EMPTY) }
+ val context = LocalContext.current
+
+ LaunchedEffect(viewModel.profileImg) {
+ viewModel.profileImg.collectLatest {
+ it?.let { url -> viewModel.setProfileUrl(url) }
+ }
+ }
+
+ val file = context.createImageFile()
+ val uri = FileProvider.getUriForFile(
+ Objects.requireNonNull(context),
+ context.packageName + ".provider", file
+ )
+
+ val cameraLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.TakePicture()) {
+ capturedUri = uri
+ viewModel.setProfileUrl(uri.toString())
+ }
+
+ val cameraPermissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) {
+ if (it) {
+ cameraLauncher.launch(uri)
+ } else {
+ // 권한 거부시
+ }
+ }
+
+ val bottomSheetState = rememberModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.Hidden
+ )
+
+ val coroutineScope = rememberCoroutineScope()
+
+ BackHandler(enabled = bottomSheetState.isVisible) {
+ coroutineScope.launch {
+ bottomSheetState.hide()
+ }
+ }
+
+ ModalBottomSheetLayout(
+ sheetState = bottomSheetState,
+ sheetShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
+ sheetContent = {
+ Column(
+ modifier = Modifier
+ .wrapContentHeight()
+ .padding(start = 20.dp, end = 20.dp, top = 18.dp, bottom = 26.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "프로필 사진 변경",
+ style = WalkieTypography.SubTitle
+ )
+
+ Spacer(modifier = Modifier.height(15.dp))
+
+ WalkieBottomSheetButton(
+ buttonText = "새 프로필 사진 찍기",
+ onClick = {
+ cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ WalkieBottomSheetButton(
+ buttonText = "앨범에서 프로필 사진 가져오기",
+ onClick = {
+ // 앨범 실행
+ }
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ WalkieBottomSheetButton(
+ buttonText = "현재 프로필 사진 삭제",
+ onClick = { }
+ )
+ }
+ }
) {
- navController.popBackStack()
+ EditProfileContent(
+ walkieId = walkieId.value ?: 0L,
+ name = name.value.orEmpty(),
+ nick = nick.value.orEmpty(),
+ bottomSheetState = bottomSheetState,
+ viewModel = viewModel,
+ ) {
+ navController.popBackStack()
+ }
}
+
+
}
+@OptIn(ExperimentalMaterialApi::class)
@Composable
fun EditProfileContent(
walkieId: Long,
name: String,
nick: String,
- profileImg: String,
+ bottomSheetState: ModalBottomSheetState,
viewModel: EditProfileViewModel,
popBackStack: () -> Unit
) {
@@ -86,10 +189,28 @@ fun EditProfileContent(
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
+ val focusManager = LocalFocusManager.current
val keyboardHeight = WindowInsets.ime.getBottom(LocalDensity.current)
val isDuplicateNickName by viewModel.isDuplicateNickName.collectAsStateWithLifecycle()
+ val currentProfileImg by viewModel.currentProfileUrl.collectAsStateWithLifecycle()
var isChangeEnabled by remember { mutableStateOf(false) }
+ var initialProfileImg by remember { mutableStateOf(null) }
+ LaunchedEffect(viewModel.profileImg) {
+ viewModel.profileImg.collectLatest {
+ initialProfileImg = it
+ }
+ }
+
+ if (currentProfileImg != null) {
+ LaunchedEffect(currentProfileImg) {
+ if (currentProfileImg != initialProfileImg) {
+ isChangeEnabled = true
+ }
+ }
+ }
+
+
LaunchedEffect(viewModel.isMyInfoChanged) {
viewModel.isMyInfoChanged.collectLatest {
Toast.makeText(context, "정보가 수정되었습니다.", Toast.LENGTH_SHORT).show()
@@ -141,7 +262,7 @@ fun EditProfileContent(
.size(90.dp),
) {
AsyncImage(
- model = profileImg,
+ model = currentProfileImg,
contentDescription = "프로필 이미지",
contentScale = ContentScale.Crop,
modifier = Modifier
@@ -151,7 +272,11 @@ fun EditProfileContent(
CircularIconButton(
modifier = Modifier.align(Alignment.BottomEnd),
- onClick = { /* 버튼 클릭 시 동작 */ },
+ onClick = {
+ coroutineScope.launch {
+ bottomSheetState.show()
+ }
+ },
contentDescription = "프로필 편집",
icon = ImageVector.vectorResource(id = R.drawable.ic_edit_icon),
tint = Color(0xFF999999),
@@ -229,9 +354,9 @@ fun EditProfileContent(
),
modifier = Modifier
.clickable {
- // 닉네임 중복 api 연결
viewModel.checkDuplicateNickName(nickName)
// 성공 시 editMode 해제
+ focusManager.clearFocus()
}
)
}
@@ -254,38 +379,10 @@ fun EditProfileContent(
text = "변경",
isEnabled = isChangeEnabled,
onClicked = {
- viewModel.changeMyInfo(walkieId, nickName, profileImg)
+ viewModel.changeMyInfo(walkieId, nickName, currentProfileImg)
}
)
Spacer(modifier = Modifier.height(20.dp))
}
-}
-
-@Composable
-fun CircularIconButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- contentDescription: String? = null,
- icon: ImageVector,
- tint: Color = Color.White,
- backgroundColor: Color = Color.Gray,
- iconSize: Dp = 24.dp,
- buttonSize: Dp = 48.dp,
-) {
- Box(
- modifier = modifier
- .size(buttonSize)
- .clip(CircleShape)
- .background(backgroundColor)
- .clickable(onClick = onClick),
- contentAlignment = Alignment.Center,
- ) {
- Icon(
- imageVector = icon,
- contentDescription = contentDescription,
- tint = tint,
- modifier = Modifier.size(iconSize),
- )
- }
-}
+}
\ No newline at end of file
diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt
index 2bd05d9e..7872537e 100644
--- a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt
+++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt
@@ -21,6 +21,9 @@ class EditProfileViewModel(
private val _isMyInfoChanged = MutableSharedFlow()
val isMyInfoChanged = _isMyInfoChanged.asSharedFlow()
+ private val _currentProfileUrl = MutableStateFlow(null)
+ val currentProfileUrl = _currentProfileUrl.asStateFlow()
+
val name = accountRepository.userName
val nick = accountRepository.nickName
val profileImg = accountRepository.profileUrl
@@ -38,4 +41,8 @@ class EditProfileViewModel(
accountRepository.changeMyInfo(walkieId, nickName, profileUrl)
.onSuccess { _isMyInfoChanged.emit(true) }
}
+
+ fun setProfileUrl(url: String) {
+ _currentProfileUrl.update { url }
+ }
}
diff --git a/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt b/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt
index bc9f7f46..d86de8dc 100644
--- a/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt
+++ b/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt
@@ -8,6 +8,10 @@ import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresApi
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
import kotlin.math.min
import kotlin.random.Random
@@ -73,3 +77,14 @@ fun Activity.openStatusBar() {
e.printStackTrace()
}
}
+
+fun Context.createImageFile(): File {
+ val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
+ val imageFileName = "JPEG_" + timeStamp + "_"
+ val image = File.createTempFile(
+ imageFileName,
+ ".jpg",
+ externalCacheDir
+ )
+ return image
+}
\ No newline at end of file