[Android/Flutter 교육] 50일차

MSU·2024년 3월 12일

Android-Flutter

목록 보기
53/85
post-thumbnail

게시판 프로젝트

LoginViewModel

LoginViewModel.kt 생성

// LoginViewModel.kt


class LoginViewModel : ViewModel() {
    // 아이디
    val textFieldLoginUserId = MutableLiveData<String>()
    // 비밀번호
    val textFieldLoginUserPw = MutableLiveData<String>()
    // 자동로그인
    val checkBoxLoginAuto = MutableLiveData<Boolean>()
}

fragment_login.xml 수정

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="loginViewModel"
            type="kr.co.lion.androidproject4boardapp.viewmodel.LoginViewModel" />
    </data>
  
  
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldLoginUserId"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={loginViewModel.textFieldLoginUserId}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldLoginUserPw"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text|textPassword"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={loginViewModel.textFieldLoginUserPw}" />
  
                <com.google.android.material.checkbox.MaterialCheckBox
                    android:id="@+id/checkBoxLoginAuto"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="10dp"
                    android:text="자동로그인"
                    android:textAppearance="@style/TextAppearance.AppCompat.Large"
                    android:checked="@={loginViewModel.checkBoxLoginAuto}" />

LoginFragment.kt 수정

// LoginFragment.kt



class LoginFragment : Fragment() {

    lateinit var fragmentLoginBinding: FragmentLoginBinding
    lateinit var mainActivity: MainActivity
    lateinit var loginViewModel: LoginViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        // fragmentLoginBinding = FragmentLoginBinding.inflate(inflater)
        fragmentLoginBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_login, container, false)
        loginViewModel = LoginViewModel()
        fragmentLoginBinding.loginViewModel = loginViewModel
        fragmentLoginBinding.lifecycleOwner = this
        
        mainActivity = activity as MainActivity

        settingToolbar()
        settingButtonLoginJoin()
        settingButtonLoginSubmit()

        return fragmentLoginBinding.root
    }
    
    

입력 요소 초기화

// LoginFragment.kt



    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        // fragmentLoginBinding = FragmentLoginBinding.inflate(inflater)
        fragmentLoginBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_login, container, false)
        loginViewModel = LoginViewModel()
        fragmentLoginBinding.loginViewModel = loginViewModel
        fragmentLoginBinding.lifecycleOwner = this

        mainActivity = activity as MainActivity

        settingToolbar()
        settingButtonLoginJoin()
        settingButtonLoginSubmit()
        settingInputForm()

        return fragmentLoginBinding.root
    }
    
    
    // 입력 요소들 초기화
    fun settingInputForm(){
        loginViewModel.textFieldLoginUserId.value = ""
        loginViewModel.textFieldLoginUserPw.value = ""
    }

유효성 검사

// LoginFragment.kt


    // 로그인 버튼
    fun settingButtonLoginSubmit(){
        fragmentLoginBinding.apply {
            buttonLoginSubmit.apply {
                // 버튼을 눌렀을 때
                setOnClickListener {
                    
                    // 유효성 검사
                    val chk = checkInputForm()
                    
                    // 모든 유효성 검사에 통과를 했다면
                    if(chk == true){
                        // ContentActivity를 실행한다.
                        val contentIntent = Intent(mainActivity, ContentActivity::class.java)
                        startActivity(contentIntent)
                        // MainActivity를 종료한다.
                        mainActivity.finish()    
                    }
                }
            }
        }
    }
    
    
    // 유효성 검사
    fun checkInputForm():Boolean{
        // 입력한 값들을 가져온다.
        val userId = loginViewModel.textFieldLoginUserId.value!!
        val userPw = loginViewModel.textFieldLoginUserPw.value!!

        if(userId.isEmpty()){
            Tools.showErrorDialog(mainActivity, fragmentLoginBinding.textFieldLoginUserId, "아이디 입력 오류", "아이디를 입력해주세요")
            return false
        }

        if(userPw.isEmpty()){
            Tools.showErrorDialog(mainActivity, fragmentLoginBinding.textFieldLoginUserPw, "비밀번호 입력 오류", "비밀번호를 입력해주세요")
            return false
        }

        return true
    }

AddContentViewModel

AddContentViewModel.kt 생성

// AddContentViewModel.kt


class AddContentViewModel : ViewModel() {
    // 제목
    val textFieldAddContentSubject = MutableLiveData<String>()
    // 내용
    val textFieldAddContentText = MutableLiveData<String>()
    // 게시판 종류
    val toggleAddContentType = MutableLiveData<Int>()

    // 게시판 종류를 받아 MutableLiveData에 설정하는 메서드
    fun settingContentType(contentType: ContentType){
        toggleAddContentType.value = when(contentType){
            ContentType.TYPE_ALL    -> 0
            ContentType.TYPE_FREE   -> R.id.buttonAddContentType1
            ContentType.TYPE_HUMOR  -> R.id.buttonAddContentType2
            ContentType.TYPE_SOCIETY-> R.id.buttonAddContentType3
            ContentType.TYPE_SPORTS -> R.id.buttonAddContentType4
        }
    }


    companion object {
        //====단방향==============================================================================
        // toggleAddContentType에 값을 설정했을 때 checkedButtonId 속성에 값을 반영해줄 때 호출(순방향)
        @BindingAdapter("android:checkedButtonId")
        @JvmStatic
        fun setCheckedButtonId(group: MaterialButtonToggleGroup, buttonId:Int){
            group.check(buttonId)
        }

        //====양방향==============================================================================

        // 안드로이드 OS가 현재 화면을 구성하고자 할 때 InverseBindingAdapter에 등록되어 있는 event 값을 보고
        // event에 등록되어 있는 이름과 동일한 BindingAdapter를 찾아 메서드를 호출해준다.
        // 리스너 설정을 해준다.
        @BindingAdapter("checkedButtonChangeListener")
        @JvmStatic
        fun checkedButtonChangeListener(group: MaterialButtonToggleGroup, inverseBindingListener: InverseBindingListener){
            group.addOnButtonCheckedListener { group, checkedId, isChecked ->
                inverseBindingListener.onChange()
            }
        }

        // 안드로이드 OS가 현재 화면을 구성하고자 할 때 속성에 설정된 MutableLiveData가 양방향(@=)으로 되어있을 경우
        // 참고할 데이터가 셋팅되어 있는 메서드
        // inverseBindingListener.onChange() 호출시 동작할 메서드
        @InverseBindingAdapter(attribute = "android:checkedButtonId", event = "checkedButtonChangeListener")
        @JvmStatic
        fun getCheckedButtonId(group: MaterialButtonToggleGroup):Int{
            return group.checkedButtonId
        }
    }

}

fragment_add_content.xml 수정


<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="addContentViewModel"
            type="kr.co.lion.androidproject4boardapp.viewmodel.AddContentViewModel" />
    </data>
  
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldAddContentSubject"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={addContentViewModel.textFieldAddContentSubject}" />
  
  
  

                <com.google.android.material.button.MaterialButtonToggleGroup
                    android:id="@+id/toggleAddContentType"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="10dp"
                    app:selectionRequired="true"
                    app:singleSelection="true"
                    android:checkedButtonId="@={addContentViewModel.toggleAddContentType}" >
  

                  
                  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldAddContentText"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text|textMultiLine"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={addContentViewModel.textFieldAddContentText}" />

게시판 종류를 enum클래스로 정의

// Tools.kt



// 게시판 종류를 나타내는 값을 정의한다.
enum class ContentType(var str:String, var number:Int){
    TYPE_ALL("전체게시판",0),
    TYPE_FREE("자유게시판",1),
    TYPE_HUMOR("유머게시판",2),
    TYPE_SOCIETY("시사게시판",3),
    TYPE_SPORTS("스포츠게시판",4),
}

AddContentFragment.kt 수정

// AddContentFragment.kt



class AddContentFragment : Fragment() {

    lateinit var fragmentAddContentBinding: FragmentAddContentBinding
    lateinit var contentActivity: ContentActivity
    lateinit var addContentViewModel: AddContentViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        // fragmentAddContentBinding = FragmentAddContentBinding.inflate(inflater)
        fragmentAddContentBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_add_content, container, false)
        addContentViewModel = AddContentViewModel()
        fragmentAddContentBinding.addContentViewModel = addContentViewModel
        fragmentAddContentBinding.lifecycleOwner = this
        
        contentActivity = activity as ContentActivity

        settingToolbarAddContent()

        return fragmentAddContentBinding.root
    }

입력 요소 초기화

// AddContentFragment.kt

    // 입력 요소 설정
    fun settingInputForm(){
        addContentViewModel.textFieldAddContentSubject.value = ""
        addContentViewModel.textFieldAddContentText.value = ""
        addContentViewModel.settingContentType(ContentType.TYPE_FREE)

        fragmentAddContentBinding.imageViewAddContent.setImageResource(R.drawable.panorama_24px)

        Tools.showSoftInput(contentActivity, fragmentAddContentBinding.textFieldAddContentSubject)
    }

툴바 초기화 버튼

// AddContentFragment.kt


    // 툴바 셋팅
    fun settingToolbarAddContent(){
        fragmentAddContentBinding.apply {
            toolbarAddContent.apply {
                // 타이틀
                title = "글 작성"
                // Back
                setNavigationIcon(R.drawable.arrow_back_24px)
                setNavigationOnClickListener {
                    contentActivity.removeFragment(ContentFragmentName.ADD_CONTENT_FRAGMENT)
                }
                // 메뉴
                inflateMenu(R.menu.menu_add_content)
                setOnMenuItemClickListener {
                    when(it.itemId){

                        // 초기화
                        R.id.menuItemAddContentReset -> {
                            settingInputForm()
                        }

카메라 기능

file_path.xml 생성


<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="storage/emulated/0"
        path="."/>
</paths>

AndroidManifest.xml 수정


        <!-- 촬영한 사진을 저장하는 프로바이더 -->
        <provider
            android:authorities="kr.co.lion.androidproject4boardapp.file_provider"
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path"/>
        </provider>
    </application>
</manifest>

카메라 관련 공용 메서드

// Tools.kt


class Tools {

    companion object {
    
    

        ////////// 카메라 관련 //////////

        // 촬영된 사진이 저장될 경로를 구해서 반환하는 메서드
        // authorities : AndroidManifest.xml에 등록한 File Provider의 이름
        fun getPictureUri(context:Context, authorities:String):Uri{
            // 촬영한 사진이 저장될 경로
            // 외부 저장소 중에 애플리케이션 영역 경로를 가져온다.
            val rootPath = context.getExternalFilesDir(null).toString()
            // 이미지 파일명을 포함한 경로
            val picPath = "${rootPath}/tempImage.jpg"
            // File 객체 생성
            val file = File(picPath)
            // 사진이 저장된 위치를 관리할 Uri 생성
            val contentUri = FileProvider.getUriForFile(context, authorities, file)

            return contentUri
        }

        ///// 카메라, 앨범 공통 ////////
        // 사진의 회전 각도값을 반환하는 메서드
        // ExifInterface : 사진, 영상, 소리 등의 파일에 기록한 정보
        // 위치, 날짜, 조리개값, 노출 정도 등등 다양한 정보가 기록된다.
        // ExifInterface 정보에서 사진 회전 각도값을 가져와서 그만큼 다시 돌려준다.
        fun getDegree(context:Context, uri:Uri) : Int {
            // 사진 정보를 가지고 있는 객체 가져온다.
            var exifInterface: ExifInterface? = null


            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                // 이미지 데이터를 가져올 수 있는 Content Provide의 Uri를 추출한다.
                // val photoUri = MediaStore.setRequireOriginal(uri)
                // ExifInterface 정보를 읽어올 스트림을 추출한다.

                val inputStream = context.contentResolver.openInputStream(uri)!!
                // ExifInterface 객체를 생성한다.
                exifInterface = ExifInterface(inputStream)
            } else {
                // ExifInterface 객체를 생성한다.
                exifInterface = ExifInterface(uri.path!!)
            }

            if(exifInterface != null){
                // 반환할 각도값을 담을 변수
                var degree = 0
                // ExifInterface 객체에서 회전 각도값을 가져온다.
                val ori = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)

                degree = when(ori){
                    ExifInterface.ORIENTATION_ROTATE_90 -> 90
                    ExifInterface.ORIENTATION_ROTATE_180 -> 180
                    ExifInterface.ORIENTATION_ROTATE_270 -> 270
                    else -> 0
                }

                return degree
            }

            return 0
        }

        // 회전시키는 메서드
        fun rotateBitmap(bitmap: Bitmap, degree:Float): Bitmap {
            // 회전 이미지를 생성하기 위한 변환 행렬
            val matrix = Matrix()
            matrix.postRotate(degree)

            // 회전 행렬을 적용하여 회전된 이미지를 생성한다.
            // 첫 번째 : 원본 이미지
            // 두 번째와 세번째 : 원본 이미지에서 사용할 부분의 좌측 상단 x, y 좌표
            // 네번째와 다섯번째 : 원본 이미지에서 사용할 부분의 가로 세로 길이
            // 여기에서는 이미지데이터 전체를 사용할 것이기 때문에 전체 영역으로 잡아준다.
            // 여섯번째 : 변환행렬. 적용해준 변환행렬이 무엇이냐에 따라 이미지 변형 방식이 달라진다.
            val rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)

            return rotateBitmap
        }

        // 이미지 사이즈를 조정하는 메서드
        fun resizeBitmap(bitmap: Bitmap, targetWidth:Int): Bitmap {
            // 이미지의 확대/축소 비율을 구한다.
            val ratio = targetWidth.toDouble() / bitmap.width.toDouble()
            // 세로 길이를 구한다.
            val targetHeight = (bitmap.height * ratio).toInt()
            // 크기를 조장한 Bitmap을 생성한다.
            val resizedBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false)

            return resizedBitmap
        }

        ////////// 카메라 관련 //////////
    }

}

카메라 런처 셋팅

// AddContentFragment.kt


class AddContentFragment : Fragment() {

    lateinit var fragmentAddContentBinding: FragmentAddContentBinding
    lateinit var contentActivity: ContentActivity
    lateinit var addContentViewModel: AddContentViewModel

    // Activity 실행을 위한 런처
    lateinit var cameraLauncher: ActivityResultLauncher<Intent>

    // 촬영된 사진이 저장된 경로 정보를 가지고 있는 Uri 객체
    lateinit var contentUri: Uri
    
    
    
    
    
    // 카메라 런처 설정
    fun settingCameraLauncher(){
        val contract1 = ActivityResultContracts.StartActivityForResult()
        cameraLauncher = registerForActivityResult(contract1){
            // 사진을 사용하겠다고 한 다음에 돌아왔을 경우
            if(it.resultCode == AppCompatActivity.RESULT_OK){
                // 사진 객체를 생성한다.
                val bitmap = BitmapFactory.decodeFile(contentUri.path)

                // 회전 각도값을 구한다.
                val degree = Tools.getDegree(contentActivity, contentUri)
                // 회전된 이미지를 구한다.
                val bitmap2 = Tools.rotateBitmap(bitmap, degree.toFloat())
                // 크기를 조정한 이미지를 구한다.
                val bitmap3 = Tools.resizeBitmap(bitmap2, 1024)

                fragmentAddContentBinding.imageViewAddContent.setImageBitmap(bitmap3)

                // 사진 파일을 삭제한다.
                val file = File(contentUri.path)
                file.delete()
            }
        }
    }

이미지 소스를 지정하는 방법은 MVVM구조에서 적용하기 애매해서 기존의 방법으로 진행한다.

카메라 런처 실행 메서드

// AddContentFragment.kt


    // 툴바 셋팅
    fun settingToolbarAddContent(){
        fragmentAddContentBinding.apply {
            toolbarAddContent.apply {
                // 타이틀
                title = "글 작성"
                // Back
                setNavigationIcon(R.drawable.arrow_back_24px)
                setNavigationOnClickListener {
                    contentActivity.removeFragment(ContentFragmentName.ADD_CONTENT_FRAGMENT)
                }
                // 메뉴
                inflateMenu(R.menu.menu_add_content)
                setOnMenuItemClickListener {
                    when(it.itemId){
                        // 카메라
                        R.id.menuItemAddContentCamera -> {
                            startCameraLauncher()
                        }



    // 카메라 런처를 실행하는 메서드
    fun startCameraLauncher(){
        // 촬영한 사진이 저장될 경로를 가져온다.
        contentUri = Tools.getPictureUri(contentActivity, "kr.co.lion.androidproject4boardapp.file_provider")

        if(contentUri != null){
            // 실행할 액티비티를 카메라 액티비티로 지정한다.
            // 단말기에 설치되어 있는 모든 애플리케이션이 가진 액티비티 중에 사진촬영이
            // 가능한 액티비가 실행된다.
            val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            // 이미지가 저장될 경로를 가지고 있는 Uri 객체를 인텐트에 담아준다.
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri)
            // 카메라 액티비티 실행
            cameraLauncher.launch(cameraIntent)
        }
    }

앨범 기능

AndroidManifest.xml 권한 추가

    <!-- 앨범으로 부터 사진을 가져오기 위한 권한 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
</manifest>

MainActivity.kt 권한 등록

// MainActivity.kt



class MainActivity : AppCompatActivity() {

    lateinit var activityMainBinding: ActivityMainBinding

    // 프래그먼트의 주소값을 담을 프로퍼티
    var oldFragment:Fragment? = null
    var newFragment:Fragment? = null

    // 확인할 권한 목록
    val permissionList = arrayOf(
        android.Manifest.permission.READ_EXTERNAL_STORAGE,
        android.Manifest.permission.ACCESS_MEDIA_LOCATION
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 스플래시 스크린이 나타나게 한다.
        // 반드시  setContentView 전에 해야 한다.
        // 코틀린에서는 installSplashScreen 메서드가 구현이 되어있어 호출만 하면 된다.
        installSplashScreen()

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        // 권한 확인
        requestPermissions(permissionList, 0)

        // 첫 화면을 띄워준다.
        replaceFragment(MainFragmentName.LOGIN_FRAGMENT, false, false, null)
    }

앨범 런처 설정

// AddContentFragment.kt


class AddContentFragment : Fragment() {

    lateinit var fragmentAddContentBinding: FragmentAddContentBinding
    lateinit var contentActivity: ContentActivity
    lateinit var addContentViewModel: AddContentViewModel

    // Activity 실행을 위한 런처
    lateinit var cameraLauncher: ActivityResultLauncher<Intent>
    lateinit var albumLauncher:ActivityResultLauncher<Intent>
    
    
    
    
    
    
    // 앨범 런처 설정
    fun settingAlbumLauncher() {
        // 앨범 실행을 위한 런처
        val contract2 = ActivityResultContracts.StartActivityForResult()
        albumLauncher = registerForActivityResult(contract2){
            // 사진 선택을 완료한 후 돌아왔다면
            if(it.resultCode == AppCompatActivity.RESULT_OK){
                // 선택한 이미지의 경로 데이터를 관리하는 Uri 객체를 추출한다.
                val uri = it.data?.data
                if(uri != null){
                    // 안드로이드 Q(10) 이상이라면
                    val bitmap = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                        // 이미지를 생성할 수 있는 객체를 생성한다.
                        val source = ImageDecoder.createSource(contentActivity.contentResolver, uri)
                        // Bitmap을 생성한다.
                        ImageDecoder.decodeBitmap(source)
                    } else {
                        // 컨텐츠 프로바이더를 통해 이미지 데이터에 접근한다.
                        val cursor = contentActivity.contentResolver.query(uri, null, null, null, null)
                        if(cursor != null){
                            cursor.moveToNext()

                            // 이미지의 경로를 가져온다.
                            val idx = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
                            val source = cursor.getString(idx)

                            // 이미지를 생성한다
                            BitmapFactory.decodeFile(source)
                        }  else {
                            null
                        }
                    }

                    // 회전 각도값을 가져온다.
                    val degree = Tools.getDegree(contentActivity, uri)
                    // 회전 이미지를 가져온다
                    val bitmap2 = Tools.rotateBitmap(bitmap!!, degree.toFloat())
                    // 크기를 줄인 이미지를 가져온다.
                    val bitmap3 = Tools.resizeBitmap(bitmap2, 1024)

                    fragmentAddContentBinding.imageViewAddContent.setImageBitmap(bitmap3)
                }
            }
        }
    }

앨범 런처를 실행하는 메서드

// AddContentFragment.kt



    // 툴바 셋팅
    fun settingToolbarAddContent(){
        fragmentAddContentBinding.apply {
            toolbarAddContent.apply {
                // 타이틀
                title = "글 작성"
                // Back
                setNavigationIcon(R.drawable.arrow_back_24px)
                setNavigationOnClickListener {
                    contentActivity.removeFragment(ContentFragmentName.ADD_CONTENT_FRAGMENT)
                }
                // 메뉴
                inflateMenu(R.menu.menu_add_content)
                setOnMenuItemClickListener {
                    when(it.itemId){
                        // 카메라
                        R.id.menuItemAddContentCamera -> {
                            startCameraLauncher()
                        }
                        // 앨범
                        R.id.menuItemAddContentAlbum -> {
                            startAlbumLauncher()
                        }
                        
                        

    // 앨범 런처를 실행하는 메서드
    fun startAlbumLauncher(){
        // 앨범에서 사진을 선택할 수 있도록 셋팅된 인텐트를 생성한다.
        val albumIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        // 실행할 액티비티의 타입을 설정(이미지를 선택할 수 있는 것이 뜨게 한다)
        albumIntent.setType("image/*")
        // 선택할 수 있는 파들의 MimeType을 설정한다.
        // 여기서 선택한 종류의 파일만 선택이 가능하다. 모든 이미지로 설정한다.
        val mimeType = arrayOf("image/*")
        albumIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimeType)
        // 액티비티를 실행한다.
        albumLauncher.launch(albumIntent)
    }

유효성 검사

// AddContentFragment.kt



    // 툴바 셋팅
    fun settingToolbarAddContent(){
        fragmentAddContentBinding.apply {
            toolbarAddContent.apply {
                // 타이틀
                title = "글 작성"
                // Back
                setNavigationIcon(R.drawable.arrow_back_24px)
                setNavigationOnClickListener {
                    contentActivity.removeFragment(ContentFragmentName.ADD_CONTENT_FRAGMENT)
                }
                // 메뉴
                inflateMenu(R.menu.menu_add_content)
                setOnMenuItemClickListener {
                    when(it.itemId){
                        // 카메라
                        R.id.menuItemAddContentCamera -> {
                            startCameraLauncher()
                        }
                        // 앨범
                        R.id.menuItemAddContentAlbum -> {
                            startAlbumLauncher()
                        }
                        // 초기화
                        R.id.menuItemAddContentReset -> {
                            settingInputForm()
                        }
                        // 완료
                        R.id.menuItemAddContentDone -> {
                            // 입력 요소 유효성 검사
                            val chk = checkInputForm()
                            if(chk == true){
                                // ReadContentFragment로 이동한다.
                                contentActivity.replaceFragment(ContentFragmentName.READ_CONTENT_FRAGMENT, true, true, null)
                            }
                        }
                    }

                    true
                }
            }
        }
    }
    


    // 입력 요소 유효성 검사
    fun checkInputForm():Boolean{
        // 입력한 내용을 가져온다.
        val subject = addContentViewModel.textFieldAddContentSubject.value!!
        val text = addContentViewModel.textFieldAddContentText.value!!

        if(subject.isEmpty()){
            Tools.showErrorDialog(contentActivity, fragmentAddContentBinding.textFieldAddContentSubject, "제목 입력 오류", "제목을 입력해주세요")
            return false
        }

        if(text.isEmpty()){
            Tools.showErrorDialog(contentActivity, fragmentAddContentBinding.textFieldAddContentText, "내용 입력 오류", "내용을 입력해주세요")
            return false
        }

        return true
    }

ReadContentViewModel

ReadContentViewModel 생성

ReadContentViewModel.kt 작성

// ReadContentViewModel.kt


class ReadContentViewModel : ViewModel() {
    // 제목
    val textFieldReadContentSubject = MutableLiveData<String>()
    // 게시판 종류
    val textFieldReadContentType = MutableLiveData<String>()
    // 닉네임
    val textFieldReadContentNickName = MutableLiveData<String>()
    // 작성 날짜
    val textFieldReadContentDate = MutableLiveData<String>()
    // 글 내용
    val textFieldReadContentText = MutableLiveData<String>()
}

fragment_read_content.xml 수정

보여주는 기능만하니까 단방향으로 설정한다.


<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="readContentViewModel"
            type="kr.co.lion.androidproject4boardapp.viewmodel.ReadContentViewModel" />
    </data>
  
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldReadContentSubject"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:clickable="false"
                        android:cursorVisible="false"
                        android:focusable="false"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@{readContentViewModel.textFieldReadContentSubject}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldReadContentType"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:clickable="false"
                        android:cursorVisible="false"
                        android:focusable="false"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@{readContentViewModel.textFieldReadContentType}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldReadContentNickName"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:clickable="false"
                        android:cursorVisible="false"
                        android:focusable="false"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@{readContentViewModel.textFieldReadContentNickName}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldReadContentDate"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:clickable="false"
                        android:cursorVisible="false"
                        android:focusable="false"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@{readContentViewModel.textFieldReadContentDate}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldReadContentText"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:clickable="false"
                        android:cursorVisible="false"
                        android:focusable="false"
                        android:inputType="text|textMultiLine"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@{readContentViewModel.textFieldReadContentText}" />

ReadContentFragment.kt 수정

// ReadContentFragment.kt


class ReadContentFragment : Fragment() {

    lateinit var fragmentReadContentBinding: FragmentReadContentBinding
    lateinit var contentActivity: ContentActivity

    lateinit var readContentViewModel: ReadContentViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        // fragmentReadContentBinding = FragmentReadContentBinding.inflate(inflater)
        fragmentReadContentBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_read_content, container, false)
        readContentViewModel = ReadContentViewModel()
        fragmentReadContentBinding.readContentViewModel = readContentViewModel
        fragmentReadContentBinding.lifecycleOwner = this
        
        contentActivity = activity as ContentActivity

        settingToolbar()
        settingBackButton()

        return fragmentReadContentBinding.root
    }

입력요소에 값을 넣어주는 메서드

// ReadContentFragment.kt


    // 입력 요소에 값을 넣어준다.
    fun settingInputForm(){
        readContentViewModel.textFieldReadContentSubject.value = "제목입니다"
        readContentViewModel.textFieldReadContentType.value = "자유게시판"
        readContentViewModel.textFieldReadContentNickName.value = "홍길동"
        readContentViewModel.textFieldReadContentDate.value = "2024-03-12"
        readContentViewModel.textFieldReadContentText.value = "내용입니다"
    }

ModifyContentViewModel

ModifyContentViewModel 생성

AddContentViewModel.kt에 작성한 메서드와 companion object를 동일하게 사용한다.

// ModifyContentViewModel.kt



class ModifyContentViewModel : ViewModel() {
    // 글 제목
    val textFieldModifyContentSubject = MutableLiveData<String>()
    // 게시판 종류
    val toggleModifyContentType = MutableLiveData<Int>()
    // 글 내용
    val textFieldModifyContentText = MutableLiveData<String>()


    // 게시판 종류를 받아 MutableLiveData에 설정하는 메서드
    fun settingContentType(contentType: ContentType){
        toggleModifyContentType.value = when(contentType){
            ContentType.TYPE_ALL    -> 0
            ContentType.TYPE_FREE   -> R.id.buttonModifyContentType1
            ContentType.TYPE_HUMOR  -> R.id.buttonModifyContentType2
            ContentType.TYPE_SOCIETY-> R.id.buttonModifyContentType3
            ContentType.TYPE_SPORTS -> R.id.buttonModifyContentType4
        }
    }


    companion object {
        //====단방향==============================================================================
        // toggleAddContentType에 값을 설정했을 때 checkedButtonId 속성에 값을 반영해줄 때 호출(순방향)
        @BindingAdapter("android:checkedButtonId")
        @JvmStatic
        fun setCheckedButtonId(group: MaterialButtonToggleGroup, buttonId:Int){
            group.check(buttonId)
        }

        //====양방향==============================================================================

        // 안드로이드 OS가 현재 화면을 구성하고자 할 때 InverseBindingAdapter에 등록되어 있는 event 값을 보고
        // event에 등록되어 있는 이름과 동일한 BindingAdapter를 찾아 메서드를 호출해준다.
        // 리스너 설정을 해준다.
        @BindingAdapter("checkedButtonChangeListener")
        @JvmStatic
        fun checkedButtonChangeListener(group: MaterialButtonToggleGroup, inverseBindingListener: InverseBindingListener){
            group.addOnButtonCheckedListener { group, checkedId, isChecked ->
                inverseBindingListener.onChange()
            }
        }

        // 안드로이드 OS가 현재 화면을 구성하고자 할 때 속성에 설정된 MutableLiveData가 양방향(@=)으로 되어있을 경우
        // 참고할 데이터가 셋팅되어 있는 메서드
        // inverseBindingListener.onChange() 호출시 동작할 메서드
        @InverseBindingAdapter(attribute = "android:checkedButtonId", event = "checkedButtonChangeListener")
        @JvmStatic
        fun getCheckedButtonId(group: MaterialButtonToggleGroup):Int{
            return group.checkedButtonId
        }
    }
}

fragment_modify_content.xml 수정


<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="modifyContentViewModel"
            type="kr.co.lion.androidproject4boardapp.viewmodel.ModifyContentViewModel" />
    </data>
  
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldModifyContentSubject"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={modifyContentViewModel.textFieldModifyContentSubject}" />
  
  
                <com.google.android.material.button.MaterialButtonToggleGroup
                    android:id="@+id/toggleModifyContentType"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="10dp"
                    app:selectionRequired="true"
                    app:singleSelection="true"
                    android:checkedButtonId="@={modifyContentViewModel.toggleModifyContentType}" >
                  
                  
                  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldModifyContentText"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text|textMultiLine"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={modifyContentViewModel.textFieldModifyContentText}" />

ModifyContentFragment.kt 수정

// ModifyContentFragment.kt



class ModifyContentFragment : Fragment() {

    lateinit var fragmentModifyContentBinding: FragmentModifyContentBinding
    lateinit var contentActivity: ContentActivity
    lateinit var modifyContentViewModel: ModifyContentViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        // fragmentModifyContentBinding = FragmentModifyContentBinding.inflate(inflater)
        fragmentModifyContentBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_modify_content, container, false)
        modifyContentViewModel = ModifyContentViewModel()
        fragmentModifyContentBinding.modifyContentViewModel = modifyContentViewModel
        fragmentModifyContentBinding.lifecycleOwner = this
        
        contentActivity = activity as ContentActivity

        settingToolbarModifyContent()

        return fragmentModifyContentBinding.root
    }

입력 요소 초기화 메서드

글 수정에서는 사용자가 직접 입력요소를 선택할 수 있도록 포커싱과 키보드를 올리지 않는다.

// ModifyContentFragment.kt


    // 입력 요소 초기화
    fun settingInputForm(){
        modifyContentViewModel.textFieldModifyContentSubject.value = "제목입니다"
        modifyContentViewModel.textFieldModifyContentText.value = "내용입니다"
        modifyContentViewModel.settingContentType(ContentType.TYPE_FREE)
    }

툴바 메뉴 버튼 설정

AddContentFragment의 코드를 가져와 수정만 해놓는다

카메라/앨범 기능

// ModifyContentFragment.kt



    // Activity 실행을 위한 런처
    lateinit var cameraLauncher: ActivityResultLauncher<Intent>
    lateinit var albumLauncher: ActivityResultLauncher<Intent>

    // 촬영된 사진이 저장된 경로 정보를 가지고 있는 Uri 객체
    lateinit var contentUri: Uri


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        // fragmentModifyContentBinding = FragmentModifyContentBinding.inflate(inflater)
        fragmentModifyContentBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_modify_content, container, false)
        modifyContentViewModel = ModifyContentViewModel()
        fragmentModifyContentBinding.modifyContentViewModel = modifyContentViewModel
        fragmentModifyContentBinding.lifecycleOwner = this

        contentActivity = activity as ContentActivity

        settingToolbarModifyContent()
        settingInputForm()
        settingCameraLauncher()
        settingAlbumLauncher()

        return fragmentModifyContentBinding.root
    }


    // 툴바 설정
    fun settingToolbarModifyContent(){
        fragmentModifyContentBinding.apply {
            toolbarModifyContent.apply {
                // 타이틀
                title = "글 수정"
                // Back
                setNavigationIcon(R.drawable.arrow_back_24px)
                setNavigationOnClickListener {
                    contentActivity.removeFragment(ContentFragmentName.MODIFY_CONTENT_FRAGMENT)
                }

                // 메뉴
                inflateMenu(R.menu.menu_modify_content)
                setOnMenuItemClickListener {
                    when (it.itemId) {
                        // 카메라
                        R.id.menuItemModifyContentCamera -> {
                            startCameraLauncher()
                        }
                        // 앨범
                        R.id.menuItemModifyContentAlbum -> {
                            startAlbumLauncher()
                        }



    // 카메라 런처 설정
    fun settingCameraLauncher(){
        val contract1 = ActivityResultContracts.StartActivityForResult()
        cameraLauncher = registerForActivityResult(contract1){
            // 사진을 사용하겠다고 한 다음에 돌아왔을 경우
            if(it.resultCode == AppCompatActivity.RESULT_OK){
                // 사진 객체를 생성한다.
                val bitmap = BitmapFactory.decodeFile(contentUri.path)

                // 회전 각도값을 구한다.
                val degree = Tools.getDegree(contentActivity, contentUri)
                // 회전된 이미지를 구한다.
                val bitmap2 = Tools.rotateBitmap(bitmap, degree.toFloat())
                // 크기를 조정한 이미지를 구한다.
                val bitmap3 = Tools.resizeBitmap(bitmap2, 1024)

                fragmentModifyContentBinding.imageViewModifyContent.setImageBitmap(bitmap3)

                // 사진 파일을 삭제한다.
                val file = File(contentUri.path)
                file.delete()
            }
        }
    }

    // 카메라 런처를 실행하는 메서드
    fun startCameraLauncher(){
        // 촬영한 사진이 저장될 경로를 가져온다.
        contentUri = Tools.getPictureUri(contentActivity, "kr.co.lion.androidproject4boardapp.file_provider")

        if(contentUri != null){
            // 실행할 액티비티를 카메라 액티비티로 지정한다.
            // 단말기에 설치되어 있는 모든 애플리케이션이 가진 액티비티 중에 사진촬영이
            // 가능한 액티비가 실행된다.
            val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            // 이미지가 저장될 경로를 가지고 있는 Uri 객체를 인텐트에 담아준다.
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri)
            // 카메라 액티비티 실행
            cameraLauncher.launch(cameraIntent)
        }
    }

    // 앨범 런처 설정
    fun settingAlbumLauncher() {
        // 앨범 실행을 위한 런처
        val contract2 = ActivityResultContracts.StartActivityForResult()
        albumLauncher = registerForActivityResult(contract2){
            // 사진 선택을 완료한 후 돌아왔다면
            if(it.resultCode == AppCompatActivity.RESULT_OK){
                // 선택한 이미지의 경로 데이터를 관리하는 Uri 객체를 추출한다.
                val uri = it.data?.data
                if(uri != null){
                    // 안드로이드 Q(10) 이상이라면
                    val bitmap = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                        // 이미지를 생성할 수 있는 객체를 생성한다.
                        val source = ImageDecoder.createSource(contentActivity.contentResolver, uri)
                        // Bitmap을 생성한다.
                        ImageDecoder.decodeBitmap(source)
                    } else {
                        // 컨텐츠 프로바이더를 통해 이미지 데이터에 접근한다.
                        val cursor = contentActivity.contentResolver.query(uri, null, null, null, null)
                        if(cursor != null){
                            cursor.moveToNext()

                            // 이미지의 경로를 가져온다.
                            val idx = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
                            val source = cursor.getString(idx)

                            // 이미지를 생성한다
                            BitmapFactory.decodeFile(source)
                        }  else {
                            null
                        }
                    }

                    // 회전 각도값을 가져온다.
                    val degree = Tools.getDegree(contentActivity, uri)
                    // 회전 이미지를 가져온다
                    val bitmap2 = Tools.rotateBitmap(bitmap!!, degree.toFloat())
                    // 크기를 줄인 이미지를 가져온다.
                    val bitmap3 = Tools.resizeBitmap(bitmap2, 1024)

                    fragmentModifyContentBinding.imageViewModifyContent.setImageBitmap(bitmap3)
                }
            }
        }
    }

    // 앨범 런처를 실행하는 메서드
    fun startAlbumLauncher(){
        // 앨범에서 사진을 선택할 수 있도록 셋팅된 인텐트를 생성한다.
        val albumIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        // 실행할 액티비티의 타입을 설정(이미지를 선택할 수 있는 것이 뜨게 한다)
        albumIntent.setType("image/*")
        // 선택할 수 있는 파들의 MimeType을 설정한다.
        // 여기서 선택한 종류의 파일만 선택이 가능하다. 모든 이미지로 설정한다.
        val mimeType = arrayOf("image/*")
        albumIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimeType)
        // 액티비티를 실행한다.
        albumLauncher.launch(albumIntent)
    }

유효성 검사 메서드

// ModifyContentFragment.kt



    // 툴바 설정
    fun settingToolbarModifyContent(){
        fragmentModifyContentBinding.apply {
            toolbarModifyContent.apply {
                // 타이틀
                title = "글 수정"
                // Back
                setNavigationIcon(R.drawable.arrow_back_24px)
                setNavigationOnClickListener {
                    contentActivity.removeFragment(ContentFragmentName.MODIFY_CONTENT_FRAGMENT)
                }

                // 메뉴
                inflateMenu(R.menu.menu_modify_content)
                setOnMenuItemClickListener {
                    when (it.itemId) {
                        // 카메라
                        R.id.menuItemModifyContentCamera -> {
                            startCameraLauncher()
                        }
                        // 앨범
                        R.id.menuItemModifyContentAlbum -> {
                            startAlbumLauncher()
                        }
                        // 초기화
                        R.id.menuItemModifyContentReset -> {
                            settingInputForm()
                        }
                        // 완료
                        R.id.menuItemModifyContentDone -> {
                            // 입력 요소 검사
                            val chk = checkInputForm()
                            if(chk == true){
                                contentActivity.removeFragment(ContentFragmentName.MODIFY_CONTENT_FRAGMENT)
                            }
                        }
                    }
                    true
                }
            }
        }
    }



    // 입력 요소의 유효성 검사
    fun checkInputForm():Boolean{
        // 입력한 내용을 가져온다.
        val subject = modifyContentViewModel.textFieldModifyContentSubject.value!!
        val text = modifyContentViewModel.textFieldModifyContentText.value!!
        
        if(subject.isEmpty()){
            Tools.showErrorDialog(contentActivity, fragmentModifyContentBinding.textFieldModifyContentSubject, "제목 입력 오류", "제목을 입력해주세요")
            return false
        }

        if(text.isEmpty()){
            Tools.showErrorDialog(contentActivity, fragmentModifyContentBinding.textFieldModifyContentText, "내용 입력 오류", "내용을 입력해주세요")
            return false
        }

        return true
    }

ModifyUserViewModel

ModifyUserViewModel 생성

ModifyUserViewModel.kt 수정

AddUserInfoViewModel 참고

// ModifyUserViewModel.kt



class ModifyUserViewModel : ViewModel() {
    // 닉네임
    val textFieldModifyUserInfoNickName = MutableLiveData<String>()
    // 나이
    val textFieldModifyUserInfoAge = MutableLiveData<String>()
    // 비밀번호
    val textFieldModifyUserInfoPw = MutableLiveData<String>()
    // 비밀번호 확인
    val textFieldModifyUserInfoPw2 = MutableLiveData<String>()
    // 성별
    val toggleModifyUserInfoGender = MutableLiveData<Int>()

    // 성별을 셋팅하는 메서드
    fun settingGender(gender: Gender){
        // 성별로 분기한다.
        when(gender){
            Gender.MALE -> {
                toggleModifyUserInfoGender.value = R.id.buttonModifyUserInfoMale
            }
            Gender.FEMALE -> {
                toggleModifyUserInfoGender.value = R.id.buttonModifyUserInfoFemale
            }
        }
    }

    companion object {
        // ViewModel에 값을 설정하여 화면에 반영하는 작업을 할 때 호출된다.
        // 괄호() 안에는 속성의 이름을 넣어준다. 속성의 이름은 자유롭게 해주면 되지만 기존의 속성 이름과 중복되지 않아야 한다.
        // 매개변수 : 값이 설정된 View 객체, ViewModel을 통해 설정되는 값
        @BindingAdapter("android:checkedButtonId")
        @JvmStatic
        fun setCheckedButtonId(group: MaterialButtonToggleGroup, buttonId:Int){
            group.check(buttonId)
        }

        // 값을 속성에 넣어주는 것을 순방향이라고 부른다.
        // 반대로 속성의 값이 변경되어 MutableLive데이터로 전달하는 것을 역방향이라고 한다.
        // 순방향만 구현해주면 단방향이 되고, 순방향과 역방향을 모두 구현해주면 양방향
        // 화면 요소가 가진 속성에 새로운 값이 설정되면 ViewModel의 변수에 값이 설정될 때 호출된다.
        // 리스너 역할을 할 속성을 만들어준다.
        @BindingAdapter("checkedButtonChangeListener")
        @JvmStatic
        fun checkedButtonChangeListener(group: MaterialButtonToggleGroup, inverseBindingListener: InverseBindingListener){
            group.addOnButtonCheckedListener { group, checkedId, isChecked ->
                inverseBindingListener.onChange()
            }
        }
        // 역방향 바인딩이 벌어질 때 호출된다.
        @InverseBindingAdapter(attribute = "android:checkedButtonId", event = "checkedButtonChangeListener")
        @JvmStatic
        fun getCheckedButtonId(group: MaterialButtonToggleGroup):Int{
            return group.checkedButtonId
        }
    }

    // 취미들
    val checkBoxModifyUserInfoHobby1 = MutableLiveData<Boolean>()
    val checkBoxModifyUserInfoHobby2 = MutableLiveData<Boolean>()
    val checkBoxModifyUserInfoHobby3 = MutableLiveData<Boolean>()
    val checkBoxModifyUserInfoHobby4 = MutableLiveData<Boolean>()
    val checkBoxModifyUserInfoHobby5 = MutableLiveData<Boolean>()
    val checkBoxModifyUserInfoHobby6 = MutableLiveData<Boolean>()

    // 취미 전체
    val checkBoxModifyUserInfoAllState = MutableLiveData<Int>()
    val checkBoxModifyUserInfoAll = MutableLiveData<Boolean>()

    // 체크박스 전체 상태를 설정하는 메서드
    fun setCheckAll(checked:Boolean){
        checkBoxModifyUserInfoHobby1.value = checked
        checkBoxModifyUserInfoHobby2.value = checked
        checkBoxModifyUserInfoHobby3.value = checked
        checkBoxModifyUserInfoHobby4.value = checked
        checkBoxModifyUserInfoHobby5.value = checked
        checkBoxModifyUserInfoHobby6.value = checked
    }

    // 전체 취미 체크박스를 누르면
    fun onCheckBoxAllChanged(){
        // 전체 취미 체크박스의 체크 여부를 모든 체크박스에 설정해준다.
        setCheckAll(checkBoxModifyUserInfoAll.value!!)
    }

    // 각 체크박스를 누르면
    fun onCheckBoxChanged(){
        // 체크되어 있는 체크박스 개수를 담을 변수
        var checkedCnt = 0
        // 체크되어 있다면 체크되어있는 체크박스의 개수를 1 증가시킨다.
        if(checkBoxModifyUserInfoHobby1.value == true){
            checkedCnt++
        }
        if(checkBoxModifyUserInfoHobby2.value == true){
            checkedCnt++
        }
        if(checkBoxModifyUserInfoHobby3.value == true){
            checkedCnt++
        }
        if(checkBoxModifyUserInfoHobby4.value == true){
            checkedCnt++
        }
        if(checkBoxModifyUserInfoHobby5.value == true){
            checkedCnt++
        }
        if(checkBoxModifyUserInfoHobby6.value == true){
            checkedCnt++
        }

        // 체크되어 있는 것이 없다면
        if(checkedCnt == 0){
            checkBoxModifyUserInfoAll.value = false
            checkBoxModifyUserInfoAllState.value = MaterialCheckBox.STATE_UNCHECKED
        }
        // 모두 체크되어 있다면
        else if(checkedCnt == 6){
            checkBoxModifyUserInfoAll.value = true
            checkBoxModifyUserInfoAllState.value = MaterialCheckBox.STATE_CHECKED
        }
        else{
            checkBoxModifyUserInfoAllState.value = MaterialCheckBox.STATE_INDETERMINATE
        }

    }
}

fragment_modify_user.xml 수정

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="modifyUserViewModel"
            type="kr.co.lion.androidproject4boardapp.viewmodel.ModifyUserViewModel" />
    </data>
  
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldModifyUserInfoNickName"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large" 
                        android:text="@={modifyUserViewModel.textFieldModifyUserInfoNickName}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldModifyUserInfoAge"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="number"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={modifyUserViewModel.textFieldModifyUserInfoAge}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldModifyUserInfoPw1"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text|textPassword"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={modifyUserViewModel.textFieldModifyUserInfoPw}" />
  
                    <com.google.android.material.textfield.TextInputEditText
                        android:id="@+id/textFieldModifyUserInfoPw2"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:inputType="text|textPassword"
                        android:textAppearance="@style/TextAppearance.AppCompat.Large"
                        android:text="@={modifyUserViewModel.textFieldModifyUserInfoPw2}" />

  
  
                <com.google.android.material.button.MaterialButtonToggleGroup
                    android:id="@+id/toggleModifyUserInfoGender"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="10dp"
                    app:selectionRequired="true"
                    app:singleSelection="true"
                    android:checkedButtonId="@={modifyUserViewModel.toggleModifyUserInfoGender}">
                  

                  
                  
                <com.google.android.material.checkbox.MaterialCheckBox
                    android:id="@+id/checkBoxModifyUserInfoAll"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="10dp"
                    android:text="취미"
                    android:textAppearance="@style/TextAppearance.AppCompat.Large"
                    app:checkedState="@{modifyUserViewModel.checkBoxModifyUserInfoAllState}"
                    android:checked="@={modifyUserViewModel.checkBoxModifyUserInfoAll}"
                    android:onClickListener="@{ (view) -> modifyUserViewModel.onCheckBoxAllChanged()}" />
                  
                  
                    <com.google.android.material.checkbox.MaterialCheckBox
                        android:id="@+id/checkBoxModifyUserInfoHobby1"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:text="운동"
                        android:checked="@={modifyUserViewModel.checkBoxModifyUserInfoHobby1}"
                        android:onClickListener="@{ (view) -> modifyUserViewModel.onCheckBoxChanged()}" />

                    <com.google.android.material.checkbox.MaterialCheckBox
                        android:id="@+id/checkBoxModifyUserInfoHobby2"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:text="독서"
                        android:checked="@={modifyUserViewModel.checkBoxModifyUserInfoHobby2}"
                        android:onClickListener="@{ (view) -> modifyUserViewModel.onCheckBoxChanged()}" />

                    <com.google.android.material.checkbox.MaterialCheckBox
                        android:id="@+id/checkBoxModifyUserInfoHobby3"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:text="영화감상"
                        android:checked="@={modifyUserViewModel.checkBoxModifyUserInfoHobby3}"
                        android:onClickListener="@{ (view) -> modifyUserViewModel.onCheckBoxChanged()}" />
                  
                  
                    <com.google.android.material.checkbox.MaterialCheckBox
                        android:id="@+id/checkBoxModifyUserInfoHobby4"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:text="요리"
                        android:checked="@={modifyUserViewModel.checkBoxModifyUserInfoHobby4}"
                        android:onClickListener="@{ (view) -> modifyUserViewModel.onCheckBoxChanged()}" />

                    <com.google.android.material.checkbox.MaterialCheckBox
                        android:id="@+id/checkBoxModifyUserInfoHobby5"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:text="음악"
                        android:checked="@={modifyUserViewModel.checkBoxModifyUserInfoHobby5}"
                        android:onClickListener="@{ (view) -> modifyUserViewModel.onCheckBoxChanged()}" />

                    <com.google.android.material.checkbox.MaterialCheckBox
                        android:id="@+id/checkBoxModifyUserInfoHobby6"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:text="기타"
                        android:checked="@={modifyUserViewModel.checkBoxModifyUserInfoHobby6}"
                        android:onClickListener="@{ (view) -> modifyUserViewModel.onCheckBoxChanged()}" />

ModifyUserFragment.kt 수정

// ModifyUserFragment.kt



class ModifyUserFragment : Fragment() {

    lateinit var fragmentModifyUserBinding: FragmentModifyUserBinding
    lateinit var contentActivity: ContentActivity
    lateinit var modifyUserViewModel: ModifyUserViewModel


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        // fragmentModifyUserBinding = FragmentModifyUserBinding.inflate(inflater)
        fragmentModifyUserBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_modify_user, container, false)
        modifyUserViewModel = ModifyUserViewModel()
        fragmentModifyUserBinding.modifyUserViewModel = modifyUserViewModel
        fragmentModifyUserBinding.lifecycleOwner = this
        
        contentActivity = activity as ContentActivity

        settingToolbarModifyUser()

        return fragmentModifyUserBinding.root
    }

입력 요소 초기화 메서드

// ModifyUserFragment.kt


    // 입력 요소 초기화
    fun settingInputForm(){
        modifyUserViewModel.textFieldModifyUserInfoNickName.value = "홍길동"
        modifyUserViewModel.textFieldModifyUserInfoAge.value = "100"
        modifyUserViewModel.textFieldModifyUserInfoPw.value = ""
        modifyUserViewModel.textFieldModifyUserInfoPw2.value = ""

        modifyUserViewModel.settingGender(Gender.FEMALE)

        modifyUserViewModel.checkBoxModifyUserInfoHobby1.value = true
        modifyUserViewModel.checkBoxModifyUserInfoHobby2.value = true
        modifyUserViewModel.checkBoxModifyUserInfoHobby3.value = false
        modifyUserViewModel.checkBoxModifyUserInfoHobby4.value = true
        modifyUserViewModel.checkBoxModifyUserInfoHobby5.value = true
        modifyUserViewModel.checkBoxModifyUserInfoHobby6.value = false

        modifyUserViewModel.onCheckBoxChanged()
    }

리셋 메뉴 버튼 추가

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/menuItemModifyUserReset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:icon="@drawable/clear_all_24px"
        android:title="초기화"
        app:showAsAction="always" />

    <item
        android:id="@+id/menuItemModifyUserDone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:icon="@drawable/done_24px"
        android:title="완료"
        app:showAsAction="always" />
</menu>
// ModifyUserFragment.kt


    fun settingToolbarModifyUser(){
        fragmentModifyUserBinding.toolbarModifyUser.apply {
            // 타이틀
            title = "회원 정보 수정"
            // 메뉴
            inflateMenu(R.menu.menu_modify_user)
            setOnMenuItemClickListener {
                when(it.itemId){
                    // 초기화
                    R.id.menuItemModifyUserReset -> {
                        settingInputForm()
                    }

유효성 검사 메서드

// ModifyUserFragment.kt


    fun settingToolbarModifyUser(){
        fragmentModifyUserBinding.toolbarModifyUser.apply {
            // 타이틀
            title = "회원 정보 수정"
            // 메뉴
            inflateMenu(R.menu.menu_modify_user)
            setOnMenuItemClickListener {
                when(it.itemId){
                    // 초기화
                    R.id.menuItemModifyUserReset -> {
                        settingInputForm()
                    }
                    // 완료
                    R.id.menuItemModifyUserDone -> {
                        // 유효성 감사를 한다.
                        val chk = checkInputForm()
                        if(chk == true){
                            Tools.hideSoftInput(contentActivity)
                        }
                    }
                }
                true
            }
        }
    }


    // 입력 요소에 대한 유효성 검사
    fun checkInputForm():Boolean {
        // 입력 요소 값들을 가져온다.
        val nickName = modifyUserViewModel.textFieldModifyUserInfoNickName.value!!
        val age = modifyUserViewModel.textFieldModifyUserInfoAge.value!!
        val pw = modifyUserViewModel.textFieldModifyUserInfoPw.value!!
        val pw2 = modifyUserViewModel.textFieldModifyUserInfoPw2.value!!

        if(nickName.isEmpty()){
            Tools.showErrorDialog(contentActivity, fragmentModifyUserBinding.textFieldModifyUserInfoNickName, "닉네임 입력 오류", "닉네임을 입력해주세요")
            return false
        }

        if(age.isEmpty()){
            Tools.showErrorDialog(contentActivity, fragmentModifyUserBinding.textFieldModifyUserInfoAge, "나이 입력 오류", "나이를 입력해주세요")
            return false
        }

        // 비밀번호 둘 중 하나라도 비어있지 않고 서로 다르다면...
        if((pw.isNotEmpty() || pw2.isNotEmpty()) && pw != pw2){
            modifyUserViewModel.textFieldModifyUserInfoPw.value = ""
            modifyUserViewModel.textFieldModifyUserInfoPw2.value = ""
            
            Tools.showErrorDialog(contentActivity, fragmentModifyUserBinding.textFieldModifyUserInfoPw2, "비밀번호 입력 오류", "비밀번호가 다릅니다")
            return false
        }

        return true
    }

메뉴 아이콘 추가

// ModifyUserFragment.kt


    fun settingToolbarModifyUser(){
        fragmentModifyUserBinding.toolbarModifyUser.apply {
            // 타이틀
            title = "회원 정보 수정"

            // 네비게이션 아이콘 설정
            setNavigationIcon(R.drawable.menu_24px)
            setNavigationOnClickListener {
                contentActivity.activityContentBinding.drawerLayoutContent.open()
            }

Firebase

Firestore : 데이터 저장 및 관리
NoSQL은 시퀀스가 없다(idx auto increment같은 기능이 없다)
직접 시퀀스 기능을 만들어야 함
join도 없다
그 외 SQL에 있는 일부 기능들이 NoSQL에서는 없다

Storage : 파일

Firebase 사용 설정

Firebase 사이트 접속
https://firebase.google.com/?hl=ko

로그인 후 페이지 상단에 있는 "Go to console" 버튼 클릭

"프로젝트 만들기" 버튼 클릭

프로젝트 이름을 짓고 동의사항 체크 후 "계속" 버튼 클릭

Google 애널리틱스 사용 설정 확인 후 "계속" 버튼 클릭
비활성화해도 상관없다

"프로젝트 만들기" 버튼 클릭

아래와 같이 프로젝트 페이지가 나오면 프로젝트 등록 완료

안드로이드 버튼을 눌러 시작하기

패키지 이름과 SHA-1를 입력하고 앱 닉네임은 안넣어도 된다.
일반적으로 패키지 이름은 앱 수준 build.gradle 파일의 applicationId이다.
SHA-1값은 안드로이드 스튜디오의 터미널에서 gradle signingReportCtrl + Enter로 입력하면 나오는 결과창에 있다.
SHA-1값은 선택사항이 아니라 필수로 넣어줘야 한다.

json파일 다운받기

내려받은 json파일을 app 폴더에 복사
app폴더는 프로젝트 폴더 보는 유형을 Project로 변경하면 된다


코틀린 프로젝트이므로 Kotlin DSL(build.gradle.kts)로 체크 확인

프로젝트 폴더에 build.gradle.kts가 2개가 있는데
첫번째는 프로젝트 수준의 파일이고 두번째는 모듈 수준의 파일이라고 보면 된다.
여태까지 수정해온 파일은 모듈 수준의 파일이고
지금 셋팅에 필요한 파일은 첫번째 파일(프로젝트 수준)이다.

페이지에 나온 코드를 파일에 복붙후 Sync Now 한다.

plugins {
    id("com.android.application") version "8.2.2" apply false
    id("org.jetbrains.kotlin.android") version "1.9.22" apply false
    id("com.google.gms.google-services") version "4.4.1" apply false
}

그 다음 모듈 수준의 코드를 복붙한다.


plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-kapt")
    id("com.google.gms.google-services")
}


dependencies {

    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    implementation("androidx.core:core-splashscreen:1.0.1")

    implementation(platform("com.google.firebase:firebase-bom:32.7.4"))
    implementation("com.google.firebase:firebase-analytics")
}

팀프로젝트(쇼핑몰)

  1. 무엇을 팔것인가
  2. 시장 조사
  3. 동종 서비스에 대한 분석(단점 뽑기)
  4. 단점들 중에 개선하고자 할 항목 선정
  5. 선정한 항목들을 적용한 어플 기획

화장품 쇼핑몰
일주일에 한번 1개월동안의 피부 변화를 사용후기로 업로드해서 보여주는 생생후기, 기간동안 꾸준히 후기 작성 시 쿠폰 발급
사진찍어서 퍼스널컬러 진단 기능

지금 나온 연예인이 입은 옷 쇼핑 플랫폼
TV나 영화에 나온 연예인이 입은 옷을 소개하고 판매하는 사이트
판매자가 직접 글을 올리거나 질문자가 질문글을 올리면 답변자가 링크를 올려준다.

기획서 내용
1. 기획의도
2. 일정
3. 팀원소개(담당 업무)
4. 어플 구조도(와이어 프레임)
5. 저장 데이터 구조도
6. 어플의 각 화면 소개
7. 향후 보완 계획
8. 소감
9. 기타

profile
안드로이드공부

0개의 댓글