Skip to content

Commit

Permalink
refactor/#68: 마이페이지 디테일 리팩토링
Browse files Browse the repository at this point in the history
  • Loading branch information
kimyuchan-k1 committed Feb 16, 2025
1 parent 3514444 commit 227f084
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.navigation.fragment.findNavController
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.example.yeongkkuel.R
import com.example.yeongkkuel.databinding.FragmentEditProfileBinding
import com.example.yeongkkuel.presentation.my.util.UriUtil
Expand All @@ -38,34 +40,35 @@ class EditProfileFragment : Fragment() {
private var _binding: FragmentEditProfileBinding? = null
private val binding get() = _binding!!

// ViewModel 연결
private val viewModel: ProfileViewModel by viewModels()
// 1) ViewModel: Activity 범위로 공유한다고 하셨으니 이렇게 지정
private val viewModel: ProfileViewModel by viewModels({ requireActivity() })

// private lateinit var imagePickerLauncher: ActivityResultLauncher<Intent>
// 실제 서버 전송용 파일

// private var selectedImageFile: File? = null
// 실제 서버 전송용 파일
private var selectedFile: File? = null

// 이미지 선택 ActivityResult
// 이미지 선택 ActivityResult (갤러리에서 사진 선택)
private val imagePickerLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
// Uri -> File (JPEG 압축) -> selectedFile
// Uri -> File
val compressedFile = UriUtil.toFile(requireContext(), it)
selectedFile = compressedFile

// 미리보기
val bitmap = BitmapFactory.decodeFile(compressedFile.absolutePath)
binding.ivProfile.setImageBitmap(bitmap)
// 필요하면 ViewModel에 경로 업데이트
// 미리보기 - imagepicker로 고른 이미지 미리보기로 보역주
Glide.with(this)
.load(compressedFile) // File 객체도 load 가능
.circleCrop() // 원형 크롭
.into(binding.ivProfile)
// 기존 로직대로, ViewModel에도 파일 경로 업데이트 (원하면 추가)
viewModel.updateProfileImageUrl(compressedFile.absolutePath)
}
}
}

private var selectedGender: View? = null
private var selectedAge: View? = null
private var selectedJob: View? = null
Expand All @@ -81,62 +84,14 @@ class EditProfileFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// 갤러리에서 이미지 가져오기 위한 launcher 등록
/*imagePickerLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
// ContentResolver를 사용하여 선택한 이미지 데이터를 임시 파일로 복사
val tempFile = createFileFromUri(it)
if (tempFile != null) {
selectedImageFile = tempFile // 실제 파일 보관
// 파일로부터 Bitmap 생성 (캐시된 파일을 읽어오기 때문에 FileNotFound 오류 방지)
val bitmap = BitmapFactory.decodeFile(tempFile.absolutePath)
val circularBitmap = bitmap.toCircularBitmap()
binding.ivProfile.setImageBitmap(circularBitmap)
// ViewModel에 파일의 경로(또는 필요에 따라 URI)를 업데이트 (API 전송 시 사용)
viewModel.updateProfileImageUrl(tempFile.absolutePath)
} else {
Toast.makeText(requireContext(), "이미지 로드 실패", Toast.LENGTH_SHORT).show()
}
}
}
}*/
/*imagePickerLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
val tempFile = createFileFromUri(it)
if (tempFile != null) {
selectedImageFile = tempFile // 실제 파일 저장
// Bitmap 생성 및 원형 변환
val bitmap = BitmapFactory.decodeFile(tempFile.absolutePath)
val circularBitmap = bitmap.toCircularBitmap()
binding.ivProfile.setImageBitmap(circularBitmap)
// 필요 시 ViewModel에 파일 경로 업데이트 (여기서는 저장 시 파일 객체를 직접 전달)
viewModel.updateProfileImageUrl(tempFile.absolutePath)
} else {
Toast.makeText(requireContext(), "이미지 로드 실패", Toast.LENGTH_SHORT).show()
}
}
}
}*/



// 화면 진입 시 기존 프로필 불러오기
// viewModel.fetchUserProfile()
// (1) 라디오 버튼(단일 선택) 세팅
setupSelectableViews()
setupListeners()
observeViewModel()

// (2) 이벤트 리스너
setupListeners()

// (3) LiveData 관찰
observeViewModel()
}

private fun setupSelectableViews() {
Expand Down Expand Up @@ -203,19 +158,25 @@ class EditProfileFragment : Fragment() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})

// 이미지 편집 버튼
// 이미지 선택 버튼
binding.tvProfileImageEdit.setOnClickListener {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
imagePickerLauncher.launch(intent)
}

// 저장 버튼
// 저장 버튼 클릭
binding.tvEdit.setOnClickListener {
if (validateSelection()) {
// ViewModel 메서드 호출
viewModel.saveUserProfile(selectedFile)
Toast.makeText(requireContext(), "프로필이 저장되었습니다.", Toast.LENGTH_SHORT).show()
findNavController().navigateUp()

// 이미지 파일이 null 이 아닌 경우
if (selectedFile != null) {
viewModel.saveUserProfile(selectedFile)
} else {
// 이미지 파일이 null 인 경우
val existingImageUrl = viewModel.profileResponse.value?.result?.profileImageUrl
viewModel.saveUserProfile(existingImageUrl?.let { File(it) })

}
}
}

Expand All @@ -225,22 +186,6 @@ class EditProfileFragment : Fragment() {
}
}

/*fun Bitmap.toCircularBitmap(): Bitmap {
val size = minOf(width, height)
val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
val rect = Rect(0, 0, size, size)
val rectF = RectF(rect)
canvas.drawOval(rectF, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(this, rect, rect, paint)
return output
}*/

private fun validateSelection(): Boolean {
val gender = (selectedGender as? TextView)?.text?.toString()
val ageGroup = (selectedAge as? TextView)?.text?.toString()
Expand All @@ -254,56 +199,69 @@ class EditProfileFragment : Fragment() {
}

private fun observeViewModel() {
// 조회/수정 결과 모두 이 LiveData로 받아서 처리

// (A) 기존 profileResponse 관찰 - 프로필 조회 / 수정 직후 결과
viewModel.profileResponse.observe(viewLifecycleOwner) { response ->
// 서버 응답이 성공일 때
if (response.isSuccess) {
response.result?.let { result ->
// 조회 시: UI에 기존 값들 반영

// 1) 닉네임 세팅
binding.etNickname.setText(result.nickname)
result.profileImageUrl?.takeIf { it.isNotEmpty() }?.let { url ->
val imageUrl = result.profileImageUrl // "https://yeongkkeul-s3.s3.ap-northeast-2.amazonaws.com/user-profile/11"
binding.tvNicknameCount.text = "${result.nickname.length}/10"

// 2) 이미지 로드: 캐시 무효화를 위해 diskCacheStrategy, skipMemoryCache 설정
val imageUrl = result.profileImageUrl
if (!imageUrl.isNullOrEmpty()) {
Glide.with(this)
.load(imageUrl)
.placeholder(R.drawable.ic_my_profile) // 로딩 중 표시
.error(R.drawable.ic_my_profile) // 에러 시 표시
.diskCacheStrategy(DiskCacheStrategy.NONE) // 캐시 끔
.skipMemoryCache(true)
.placeholder(null)
.error(R.drawable.bg_box_white)
.circleCrop()
.into(binding.ivProfile)
} ?: run {
// 기본 이미지 설정 또는 아무 작업도 하지 않음
} else {
binding.ivProfile.setImageResource(R.drawable.ic_my_profile)
}
binding.tvNicknameCount.text = "${result.nickname.length}/10"

// 기존에 선택된 값들도 반영
// 3) 기존에 선택된 값들
val displayAgeGroup = convertAgeGroup(result.ageGroup)
val displayGenderGroup = convertGender(result.gender)
val displayGenderGroup = convertGender(result.gender)
val displayJobGroup = convertJob(result.job)

updateInitialSelection(binding.glGenderGroup,displayGenderGroup )
updateInitialSelection(binding.glGenderGroup, displayGenderGroup)
updateInitialSelection(binding.glAgeGroup, displayAgeGroup)
updateInitialSelection(binding.glJobGroup, displayJobGroup)
}
} else {
// 에러 처리
Toast.makeText(requireContext(),
Toast.makeText(
requireContext(),
"실패: ${response.message ?: "오류가 발생했습니다."}",
Toast.LENGTH_SHORT
).show()
}
}

viewModel.updateStatus.observe(viewLifecycleOwner) { result ->
result.onSuccess {
// PATCH 요청이 정상적으로 완료된 시점
Toast.makeText(requireContext(), "프로필이 저장되었습니다.", Toast.LENGTH_SHORT).show()
findNavController().navigateUp()
}.onFailure { e ->
Toast.makeText(requireContext(), "수정 실패: ${e.message}", Toast.LENGTH_SHORT).show()
// (B) 이벤트 관찰 - 수정 성공/실패
viewModel.updateStatusEvent.observe(viewLifecycleOwner) { event ->
// 이벤트가 이미 처리되었는지 확인
event.getContentIfNotHandled()?.let { isSuccess ->
if (isSuccess) {
// 성공
Toast.makeText(requireContext(), "프로필이 저장되었습니다.", Toast.LENGTH_SHORT).show()
findNavController().navigateUp()
} else {
// 실패
Toast.makeText(requireContext(), "프로필 수정 실패", Toast.LENGTH_SHORT).show()
}
}
}

}

// --------------------------------------------------------------------------------
// 이하 변환 함수(기존 로직 유지)
// --------------------------------------------------------------------------------

private fun updateInitialSelection(group: ViewGroup, value: String) {
for (i in 0 until group.childCount) {
val child = group.getChildAt(i) as? TextView
Expand Down Expand Up @@ -332,31 +290,29 @@ class EditProfileFragment : Fragment() {
"FORTIES" -> "40대"
"FIFTIES" -> "50대"
"SIXTIES_AND_ABOVE" -> "60대 이상"
else -> apiAgeGroup // 추가 케이스가 있으면 여기에 추가
else -> apiAgeGroup
}
}
// 성별 변환

private fun convertGender(apiGender: String): String {
return when (apiGender.uppercase()) {
"FEMALE" -> "여자"
"MALE" -> "남자"
else -> apiGender // 추가 케이스가 있으면 여기에 추가
else -> apiGender
}
}

// 직업 변환
private fun convertJob(apiJob: String): String {
return when (apiJob.uppercase()) {
"STUDENT" -> "학생"
"EMPLOYEE" -> "직장인"
"SELF_EMPLOYED" -> "자영업자"
"HOMEMAKER" -> "주부"
"UNDECIDED" -> "무직"
else -> apiJob // 추가 케이스가 있으면 여기에 추가
else -> apiJob
}
}

// 반대로 변환
private fun convertBackAgeGroup(displayAgeGroup: String): String {
return when (displayAgeGroup) {
"14~19세" -> "TEENAGER"
Expand Down Expand Up @@ -387,41 +343,8 @@ class EditProfileFragment : Fragment() {
}
}


// 임시 파일 생성 함수
private fun createFileFromUri(uri: Uri): File? {
return try {
// 캐시 디렉토리에 임시 파일 생성
val inputStream = requireContext().contentResolver.openInputStream(uri) ?: return null
val tempFile = File.createTempFile("profile_image", ".png", requireContext().cacheDir)
tempFile.outputStream().use { output ->
inputStream.copyTo(output)
}
tempFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}

// Bitmap을 원형으로 변환하는 확장 함수
fun Bitmap.toCircularBitmap(): Bitmap {
val size = minOf(width, height)
val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
val rect = Rect(0, 0, size, size)
val rectF = RectF(rect)
canvas.drawOval(rectF, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(this, rect, rect, paint)
return output
}



override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/example/yeongkkuel/presentation/my/Event.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.yeongkkuel.presentation.my

open class Event<out T>(private val content: T) {

private var hasBeenHandled = false

fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}

/**
* 이벤트 처리 여부와 상관없이 값을 반환 (디버깅 등에서 사용)
*/
fun peekContent(): T = content
}
Loading

0 comments on commit 227f084

Please sign in to comment.