맨땅에 코딩

'학습의 숲' 소소한 개발일지 - 나의 시간표 본문

KAU 2023 (2학년)/객체지향프로그래밍

'학습의 숲' 소소한 개발일지 - 나의 시간표

나는 푸딩 2024. 7. 25. 21:26

TimetableRepository

🍀 Firebase Realtime Database를 통해 사용자의 강의 데이터를 가져오고, 새로운 강의를 추가하고, 기존 강의를 삭제하는 기능을 제공한다. 이를 통해 앱의 다른 부분에서 강의 데이터를 쉽게 관리할 수 있다.

 

package com.example.forestlearning.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.forestlearning.CourseData
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener

class TimetableRepository {
    // Firebase 인스턴스 초기화
    private val db = FirebaseDatabase.getInstance().reference
    private val auth = FirebaseAuth.getInstance()

    //사용자의 강의 데이터를 가져오는 함수
    fun getCourses(): LiveData<List<CourseData>> {
        //사용자의 UID를 가져옴
        val userId = auth.currentUser?.uid ?: return MutableLiveData(emptyList())
        val liveData = MutableLiveData<List<CourseData>>()

        //Firebase에서 강의 데이터 가져오기
        getCoursesReference(userId).addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                // snapshot에서 강의 데이터를 가져와서 LiveData의 value로 설정
                val courses = snapshot.children.mapNotNull {
                    it.getValue(CourseData::class.java)
                }
                liveData.value = courses
            }

            override fun onCancelled(error: DatabaseError) {
                // 데이터를 가져오는 것이 취소되었을 때의 처리
            }
        })
        
        return liveData
    }

    // 강의를 추가하는 함수
    fun addCourse(courseData: CourseData){
        // 현재 사용자의 UID를 가져오기
        val userId = auth.currentUser?.uid ?: return
        // 새로운 강의 ID를 생성
        val courseId = getCoursesReference(userId).push().key ?: return
        // Firebase에 새로운 강의 데이터를 추가
        getCoursesReference(userId).child(courseId).setValue(courseData)
            .addOnSuccessListener {
                // 데이터 추가에 성공
            }
            .addOnFailureListener {
                // 데이터 추가에 실패
            }
    }

    fun deleteCourseByName(courseName: String) {
        val userId = auth.currentUser?.uid ?: return
        getCoursesReference(userId).addListenerForSingleValueEvent(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                snapshot.children.forEach { dataSnapshot ->
                    val course = dataSnapshot.getValue(CourseData::class.java)
                    if (course?.courseName == courseName) {
                        dataSnapshot.ref.removeValue() // 해당 강의를 삭제
                    }
                }
            }

            override fun onCancelled(error: DatabaseError) {
                // 데이터를 가져오는 것이 취소되었을 때의 처리
            }
        })
    }

    // 강의 데이터를 저장하고 있는 Firebase의 경로를 가져오는 함수
    private fun getCoursesReference(userId: String) = db.child("Courses").child(userId)
}

 

  1. db: Firebase Realtime Database의 참조다. 이를 통해 데이터베이스에 접근할 수 있다.

  2. auth: Firebase Authentication의 인스턴스다. 이를 통해 현재 로그인한 사용자의 정보를 가져올 수 있다.

  3. getCourses(): 현재 로그인한 사용자의 강의 데이터를 가져오는 함수다. 사용자의 UID를 통해 해당 사용자의 강의 데이터를 Firebase에서 가져오며, 이를 LiveData로 반환한다. 이렇게 함으로써 UI에서 이 LiveData를 관찰하면서 강의 데이터가 변경될 때마다 UI를 업데이트할 수 있다.

  4. addCourse(): 새로운 강의를 추가하는 함수다. 사용자의 UID를 통해 해당 사용자의 강의 데이터 위치에 새로운 강의를 추가한다.

  5. deleteCourseByName(): 해당 이름의 강의를 삭제하는 함수다. 사용자의 UID를 통해 해당 사용자의 강의 데이터를 가져오고, 그 중에서 특정 이름의 강의를 찾아 삭제한다.

  6. getCoursesReference(): 사용자의 UID를 인자로 받아 해당 사용자의 강의 데이터를 저장하고 있는 Firebase의 경로를 반환하는 함수이다.

TimetableViewModel

🍀 강의 정보를 LiveData로 관리하고, 이 정보를 Firebase Realtime Database와 동기화하는 역할을 한다. 이를 통해 앱의 다른 부분에서 강의 정보를 쉽게 관찰하고 업데이트할 수 있다.

 

package com.example.forestlearning.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.forestlearning.CourseData
import com.example.forestlearning.repository.TimetableRepository

class TimeTableViewModel : ViewModel(){
    // TimetableRepository 인스턴스 생성
    private val repository = TimetableRepository()
    // CourseData를 관리하는 MutableLiveData 생성
    private val _courseData = MutableLiveData<CourseData>()

    val courseData : MutableLiveData<CourseData> get() = _courseData

    // repository로부터 강의 정보를 가져옴
    var courses: LiveData<List<CourseData>> = repository.getCourses()

    // 강의 데이터를 설정하고 repository에 추가하는 함수
    fun setCourseData(courseName: String, teacherName: String, day1: String, time1: String, time2: String, coursePlace1: String, day2: String, time3: String, time4: String, coursePlace2: String){
        val courseData = CourseData(courseName, teacherName, day1, time1, time2, coursePlace1, day2, time3, time4, coursePlace2)
        _courseData.value = courseData
        repository.addCourse(courseData)
    }

    // 강의 데이터를 삭제하는 함수
    fun resetCourseData(courseName: String) {
        // repository에서 강의 삭제
        repository.deleteCourseByName(courseName)
        // LiveData 갱신
        courses = repository.getCourses()
    }
}

 

  1. repository: TimetableRepository의 인스턴스다. 이를 통해 강의 데이터를 Firebase Realtime Database에 저장하거나 가져올 수 있다.

  2. _courseData: 강의 데이터를 저장하는 MutableLiveData다. MutableLiveData는 값이 변경될 수 있는 LiveData다. LiveData는 데이터의 변경을 관찰하고 UI를 업데이트하는데 사용된다.

  3. courseData: _courseData의 getter다. 이를 통해 TimeTableViewModel 외부에서는 LiveData를 읽기 전용으로 접근할 수 있다.

  4. courses: repository로부터 가져온 강의 리스트를 저장하는 LiveData다.

  5. setCourseData(): 강의 데이터를 설정하고 Firebase Realtime Database에 저장하는 함수입니다. 여러 강의 정보를 파라미터로 받아 CourseData 객체를 생성하고, 이를 MutableLiveData와 Firebase Realtime Database에 저장한다.

  6. resetCourseData(): 특정 강의 데이터를 삭제하는 함수다. 강의 이름을 파라미터로 받아 repository에서 해당 강의를 삭제하고, 강의 리스트를 다시 가져와 courses를 업데이트한다.

TimetableFragment

🍀 사용자의 강의 시간표를 화면에 보여주고, 강의 정보가 변경될 때마다 화면을 업데이트하는 역할을 한다. 또한 사용자가 새로운 강의를 추가할 수 있도록 courseAddFragment로 이동하는 기능과 강의를 삭제하는 기능을 제공한다. 이를 통해 사용자는 강의 시간표를 쉽게 확인하고, 필요에 따라 강의를 추가하는 화면으로 이동하거나 강의를 삭제할 수 있다.

 

package com.example.forestlearning

import android.app.AlertDialog
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.example.forestlearning.databinding.FragmentTimetableBinding
import com.example.forestlearning.viewmodel.TimeTableViewModel

class TimetableFragment : Fragment() {

    private lateinit var binding: FragmentTimetableBinding
    private val viewModel : TimeTableViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentTimetableBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val dayTimeMap = initDayTimeMap()

        viewModel.courses.observe(viewLifecycleOwner) { courses ->
            // 강의 정보 지우기
            dayTimeMap.values.forEach { view ->
                if (view is TextView) {
                    view.text = ""
                }
            }

            // 강의 정보를 설정하기
            courses.forEach { course ->
                setCourseInfo(dayTimeMap, course, course.day1, course.coursePlace1, course.time1?.let { convertTimeToMinute(it) }, course.time2?.let { convertTimeToMinute(it) })
                setCourseInfo(dayTimeMap, course, course.day2, course.coursePlace2, course.time3?.let { convertTimeToMinute(it) }, course.time4?.let { convertTimeToMinute(it) })
            }
        }

        //courseAddButton 클릭 시 courseAddFragment로 이동
        binding.courseAddButton.setOnClickListener {
            findNavController().navigate(R.id.action_timetableFragment_to_courseAddFragment)
        }

    }

    //시간을 분 단위로 반환하는 함수
    private fun convertTimeToMinute(time: String): Int {
        val split = time.split(":")
        val hours = split[0].toInt()
        val minutes = split[1].toInt()
        return hours * 60 + minutes
    }

    //분 단위를 시간 형식으로 변환하는 함수
    private fun convertMinuteToTime(minute: Int): String {
        val hours = minute / 60
        val minutes = minute % 60
        return String.format("%02d:%02d", hours, minutes)
    }

    //요일과 시간을 키로, 연결하는 맵을 초기화하는 함수
    private fun initDayTimeMap() = mapOf(
        "월" to listOf(binding.monday0, binding.monday1, binding.monday2, binding.monday3, binding.monday4, binding.monday5, binding.monday6, binding.monday7, binding.monday8),
        "화" to listOf(binding.tuesday0, binding.tuesday1, binding.tuesday2, binding.tuesday3, binding.tuesday4, binding.tuesday5, binding.tuesday6, binding.tuesday7, binding.tuesday8),
        "수" to listOf(binding.wednesday0, binding.wednesday1, binding.wednesday2, binding.wednesday3, binding.wednesday4, binding.wednesday5, binding.wednesday6, binding.wednesday7, binding.wednesday8),
        "목" to listOf(binding.thursday0, binding.thursday1, binding.thursday2, binding.thursday3, binding.thursday4, binding.thursday5, binding.thursday6, binding.thursday7, binding.thursday8),
        "금" to listOf(binding.friday0, binding.friday1, binding.friday2, binding.friday3, binding.friday4, binding.friday5, binding.friday6, binding.friday7, binding.friday8)
    ).flatMap { entry ->
        val day = entry.key
        val times = entry.value
        times.indices.map { index ->
            val time = convertMinuteToTime(index * 60 + 9 * 60) // 9시부터 시작
            "${day}${time}-${convertMinuteToTime(index * 60 + 10 * 60)}" to times[index] // 1시간 간격
        }
    }.toMap()

    //강의 정보를 시간에 맞게 설정하는 함수
    private fun setCourseInfo(dayTimeMap: Map<String, View>, course: CourseData, day: String?, place: String?, startTime: Int?, endTime: Int?) {
        startTime?.let { start ->
            endTime?.let { end ->
                for (time in start until end step 60) {
                    val key = "$day${convertMinuteToTime(time)}-${convertMinuteToTime(time + 60)}"
                    val textView = dayTimeMap[key] as? TextView
                    textView?.apply {
                        text = "${course.courseName}\n${course.teacherName}\n$place"
                        val context = context // context를 미리 가져옴
                        setOnClickListener {
                            context?.let {
                                AlertDialog.Builder(it)
                                    .setTitle("강의 삭제")  // 대화상자의 제목 설정
                                    .setMessage("${course.courseName} 강의를 삭제하시겠습니까?")  // 대화상자의 메시지 설정
                                    .setPositiveButton("예") { dialog, which ->
                                        // "예" 버튼이 눌렸을 때의 동작 설정
                                        viewModel.resetCourseData(course.courseName ?: "")  // 강의명 전달

                                    }
                                    .setNegativeButton("아니오", null)  // "아니오" 버튼이 눌렸을 때의 동작 설정
                                    .show()  // 대화 상자 표시
                            }
                        }
                    }
                }
            }
        }
    }
}

 

  1. binding: FragmentTimetableBinding의 인스턴스다. 이를 통해 레이아웃의 뷰에 접근할 수 있다.

  2. viewModel: TimeTableViewModel의 인스턴스다. 이를 통해 강의 데이터를 가져오거나 설정할 수 있다.

  3. onCreateView(): 프래그먼트의 뷰를 생성하는 함수다. 여기서 FragmentTimetableBinding을 통해 인플레이션을 진행하고, 그 결과로 생성된 뷰를 반환한다.

  4. onViewCreated(): 프래그먼트의 뷰가 생성된 후 호출되는 함수다. 여기서 주로 뷰의 초기 설정을 진행하며, 이 예제에서는 initDayTimeMap() 함수를 통해 요일과 시간에 따른 뷰의 매핑을 설정하고, viewModel.courses를 관찰하여 강의 정보가 변경될 때마다 화면을 업데이트한다.

  5. convertTimeToMinute(): 시간을 분으로 변환하는 함수다. 이 함수는 코드에서는 보이지 않지만, 예를 들어 "10:30"이라는 시간을 630분으로 변환하는 역할을 할 것이다.

  6. courseAddButton 클릭 리스너: courseAddButton을 클릭하면 courseAddFragment로 이동한다.

  7. convertTimeToMinute(): 시간을 분으로 변환하는 함수다. 예를 들어 "10:30"이라는 시간을 받으면, 이를 630분으로 변환하여 반환한다.

  8. convertMinuteToTime(): 분을 시간 형식으로 변환하는 함수다. 예를 들어 630을 받으면, 이를 "10:30"으로 변환하여 반환한다.

  9. initDayTimeMap(): 요일과 시간에 따른 뷰의 매핑을 설정하는 함수다. 각 요일과 시간에 해당하는 뷰를 맵 형태로 관리하여, 강의 정보를 화면에 표시할 때 이를 쉽게 찾을 수 있도록 한다.

  10. setCourseInfo(): 강의 정보를 화면에 설정하는 함수입니다. 강의의 요일, 장소, 시작 시간, 종료 시간을 파라미터로 받아, 해당 정보를 바탕으로 화면에 강의 정보를 표시한다. 또한 강의 정보가 표시된 뷰에 클릭 리스너를 설정하여, 뷰를 클릭하면 해당 강의를 삭제할 것인지 묻는 대화 상자를 표시한다.

CourseAddFragment

🍀 사용자가 새로운 강의를 추가할 수 있도록 하며, 추가된 강의는 TimeTableViewModel을 통해 관리된다. 또한 사용자가 완료 버튼을 누르면 강의 시간표 화면으로 다시 돌아간다.

 

package com.example.forestlearning

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.example.forestlearning.databinding.FragmentCourseAddBinding
import com.example.forestlearning.viewmodel.TimeTableViewModel

class CourseAddFragment : Fragment() {
    //뷰 바인딩 초기화
    private var _binding: FragmentCourseAddBinding? = null
    private val binding get() = _binding
    val viewModel : TimeTableViewModel by activityViewModels()

    //스피너 변수 선언
    private var daySpinners: List<Spinner>? = null
    private var timeSpinners: List<Spinner>? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        //뷰 바인딩 설정
        _binding = FragmentCourseAddBinding.inflate(inflater, container, false)

        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //스피너 바인딩
        daySpinners = listOfNotNull(binding?.daySpinner1, binding?.daySpinner2)
        timeSpinners = listOfNotNull(binding?.timeSpinner1, binding?.timeSpinner2, binding?.timeSpinner3, binding?.timeSpinner4)

        //courseEndButton 클릭 시 동작
        binding?.courseEndButton?.setOnClickListener {
            // 강의 이름, 교수명, 강의실이 모두 입력되었는지 검사
            if (binding?.courseName?.text.isNullOrEmpty() ||
                binding?.teacherName?.text.isNullOrEmpty() ||
                binding?.coursePlace1?.text.isNullOrEmpty()) {
                Toast.makeText(requireContext(), "강의 정보를 모두 입력해주세요.", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            // time1과 time2, time3과 time4가 모두 같은지 검사
            if (timeSpinners?.getOrNull(0)?.selectedItem.toString() == timeSpinners?.getOrNull(1)?.selectedItem.toString() &&
                timeSpinners?.getOrNull(2)?.selectedItem.toString() == timeSpinners?.getOrNull(3)?.selectedItem.toString()) {
                Toast.makeText(requireContext(), "시간을 다시 설정해주세요.", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            // 강의 정보를 설정
            viewModel.setCourseData(
                binding?.courseName?.text.toString(),
                binding?.teacherName?.text.toString(),
                daySpinners?.getOrNull(0)?.selectedItem.toString(),
                timeSpinners?.getOrNull(0)?.selectedItem.toString(),
                timeSpinners?.getOrNull(1)?.selectedItem.toString(),
                binding?.coursePlace1?.text.toString(),
                daySpinners?.getOrNull(1)?.selectedItem.toString(),
                timeSpinners?.getOrNull(2)?.selectedItem.toString(),
                timeSpinners?.getOrNull(3)?.selectedItem.toString(),
                binding?.coursePlace2?.text.toString())

            // timetableFragment로 이동
            findNavController().navigate(R.id.action_courseAddFragment_to_timetableFragment)
        }

        //스피너에 어댑터 설정
        val dayAdapter = ArrayAdapter.createFromResource(requireContext(), R.array.days, android.R.layout.simple_spinner_dropdown_item)
        val timeAdapter = ArrayAdapter.createFromResource(requireContext(), R.array.time, android.R.layout.simple_spinner_dropdown_item)

        // 각각의 스피너에 어댑터 연결
        daySpinners?.forEach { it.adapter = dayAdapter }
        timeSpinners?.forEach { it.adapter = timeAdapter }

    }

    // 뷰가 파괴될 때 바인딩 해제
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

}

 

  1. binding: FragmentCourseAddBinding의 인스턴스다. 이를 통해 레이아웃의 뷰에 접근할 수 있다.

  2. viewModel: TimeTableViewModel의 인스턴스다. 이를 통해 강의 데이터를 설정할 수 있다.

  3. daySpinners, timeSpinners: 요일과 시간을 선택할 수 있는 Spinner의 리스트다.

  4. onCreateView(): 프래그먼트의 뷰를 생성하는 함수다. 여기서 FragmentCourseAddBinding을 통해 인플레이션을 진행하고, 그 결과로 생성된 뷰를 반환한다.

  5. onViewCreated(): 프래그먼트의 뷰가 생성된 후 호출되는 함수다. 여기서 주로 뷰의 초기 설정을 진행하며, 이 예제에서는 Spinner에 어댑터를 설정하고, courseEndButton에 클릭 리스너를 설정한다.

  6. courseEndButton 클릭 리스너: courseEndButton을 클릭하면 입력된 강의 정보를 검사하고, 유효한 경우 viewModel을 통해 강의를 추가한 후 timetableFragment로 이동한다.

  7. onDestroyView(): 프래그먼트의 뷰가 파괴될 때 호출되는 함수다. 여기서 binding을 해제하여 메모리 누수를 방지한다.