
// ContentAcitivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityContentBinding = ActivityContentBinding.inflate(layoutInflater)
setContentView(activityContentBinding.root)
// 로그인한 사용자 정보를 변수에 담아둔다.
loginUserIdx = intent.getIntExtra("loginUserIdx", 0)
loginUserNickName = intent.getStringExtra("loginUserNickName")!!
settingNavigationView()
val mainBundle = Bundle()
mainBundle.putString("TypeName",ContentType.TYPE_ALL.str)
mainBundle.putInt("TypeNumber",ContentType.TYPE_ALL.number)
replaceFragment(ContentFragmentName.MAIN_FRAGMENT, false, false, mainBundle)
}
// 네비게이션 뷰 설정
fun settingNavigationView(){
activityContentBinding.apply {
navigationViewContent.apply {
// 헤더로 보여줄 view를 생성한다.
val headerContentDrawerBinding = HeaderContentDrawerBinding.inflate(layoutInflater)
// 헤더로 보여줄 View를 설정한다.
addHeaderView(headerContentDrawerBinding.root)
// 사용자 닉네임을 설정한다.
headerContentDrawerBinding.headerContentDrawerNickName.text = loginUserNickName
// 메뉴를 눌렀을 때 동작하는 리스너
setNavigationItemSelectedListener {
// 딜레이를 조금 준다.
SystemClock.sleep(200)
// 메뉴의 id로 분기한다.
when(it.itemId){
// 전체 게시판
R.id.menuItemContentNavigationAll -> {
val mainBundle = Bundle()
// 게시판 종류를 담는다.
mainBundle.putString("TypeName",ContentType.TYPE_ALL.str)
mainBundle.putInt("TypeNumber",ContentType.TYPE_ALL.number)
replaceFragment(ContentFragmentName.MAIN_FRAGMENT, false, false, mainBundle)
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 자유 게시판
R.id.menuItemContentNavigation1 -> {
val mainBundle = Bundle()
// 게시판 종류를 담는다.
mainBundle.putString("TypeName",ContentType.TYPE_FREE.str)
mainBundle.putInt("TypeNumber",ContentType.TYPE_FREE.number)
replaceFragment(ContentFragmentName.MAIN_FRAGMENT, false, false, mainBundle)
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 유머 게시판
R.id.menuItemContentNavigation2 -> {
val mainBundle = Bundle()
// 게시판 종류를 담는다.
mainBundle.putString("TypeName",ContentType.TYPE_HUMOR.str)
mainBundle.putInt("TypeNumber",ContentType.TYPE_HUMOR.number)
replaceFragment(ContentFragmentName.MAIN_FRAGMENT, false, false, mainBundle)
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 시사 게시판
R.id.menuItemContentNavigation3 -> {
val mainBundle = Bundle()
// 게시판 종류를 담는다.
mainBundle.putString("TypeName",ContentType.TYPE_SOCIETY.str)
mainBundle.putInt("TypeNumber",ContentType.TYPE_SOCIETY.number)
replaceFragment(ContentFragmentName.MAIN_FRAGMENT, false, false, mainBundle)
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 스포츠 게시판
R.id.menuItemContentNavigation4 -> {
val mainBundle = Bundle()
// 게시판 종류를 담는다.
mainBundle.putString("TypeName",ContentType.TYPE_SPORTS.str)
mainBundle.putInt("TypeNumber",ContentType.TYPE_SPORTS.number)
replaceFragment(ContentFragmentName.MAIN_FRAGMENT, false, false, mainBundle)
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 사용자 정보 수정
R.id.menuItemContentNavigationModifyUserInfo -> {
replaceFragment(ContentFragmentName.MODIFY_USER_FRAGMENT, false, false, null)
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 로그아웃
R.id.menuItemContentNavigationLogout -> {
// NavigationView를 닫아준다.
drawerLayoutContent.close()
// MainActivity를 실행한다.
val mainIntent = Intent(this@ContentActivity, MainActivity::class.java)
startActivity(mainIntent)
// ContentActivity를 종료한다.
this@ContentActivity.finish()
}
// 회원탈퇴
R.id.menuItemContentNavigationSignOut -> {
// NavigationView를 닫아준다.
drawerLayoutContent.close()
// MainActivity를 실행한다.
val mainIntent = Intent(this@ContentActivity, MainActivity::class.java)
startActivity(mainIntent)
// ContentActivity를 종료한다.
this@ContentActivity.finish()
}
}
true
}
}
}
}
// MainFragment.kt
// 게시판 종류를 담을 프로퍼티
var contentType = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentMainBinding = FragmentMainBinding.inflate(inflater)
contentActivity = activity as ContentActivity
// 게시판 종류값을 담아준다.
contentType = arguments?.getInt("TypeNumber")!!
settingToolbar()
settingRecyclerViewMain()
settingRecyclerViewMainSearch()
settingSearchBar()
return fragmentMainBinding.root
}
// MainFragment.kt
// 툴바 설정
fun settingToolbar(){
fragmentMainBinding.apply {
toolbarMain.apply {
// 타이틀
title = arguments?.getString("TypeName")
// 메뉴
inflateMenu(R.menu.menu_content_main)
setNavigationIcon(R.drawable.menu_24px)
setNavigationOnClickListener {
contentActivity.activityContentBinding.drawerLayoutContent.open()
}
}
}
}


// ContentDao.kt
// 게시글 목록을 가져온다.
suspend fun gettingContentList(contentType:Int):MutableList<ContentModel>{
// 게시글 정보를 담을 리스트
val contentList = mutableListOf<ContentModel>()
val job1 = CoroutineScope(Dispatchers.IO).launch {
// 컬렉션에 접근할 수 있는 객체를 가져온다.
val collectionReference = Firebase.firestore.collection("ContentData")
// 게시글 상태가 정상
var query = collectionReference.whereEqualTo("contentState", ContentState.CONTENT_STATE_NORMAL.number)
// 게시글 번호를 기준으로 내림차순 정렬
query = query.orderBy("contentIdx",Query.Direction.DESCENDING)
// 만약 전체 게시판이 아니라면
if(contentType != ContentType.TYPE_ALL.number){
query = query.whereEqualTo("contentType", contentType)
}
val querySnapshot = query.get().await()
querySnapshot.forEach {
// 현재 번째의 문서를 객체로 받아온다.
val contentModel = it.toObject(ContentModel::class.java)
// 객체를 리스트에 담는다.
contentList.add(contentModel)
}
}
job1.join()
return contentList
}
아래와같이 where조건을 작성하고 order조건을 작성한 후에 다시 where조건을 작성한다면
val job1 = CoroutineScope(Dispatchers.IO).launch {
// 컬렉션에 접근할 수 있는 객체를 가져온다.
val collectionReference = Firebase.firestore.collection("ContentData")
// 게시글 상태가 정상
var query = collectionReference.whereEqualTo("contentState", ContentState.CONTENT_STATE_NORMAL.number)
// 게시글 번호를 기준으로 내림차순 정렬
query = query.orderBy("contentIdx",Query.Direction.DESCENDING)
// 만약 전체 게시판이 아니라면
if(contentType != ContentType.TYPE_ALL.number){
query = query.whereEqualTo("contentType", contentType)
}
val querySnapshot = query.get().await()
querySnapshot.forEach {
contentList.add(it.toObject(ContentModel::class.java))
}
}

이러한 에러가 발생하게 되는데
Caused by: io.grpc.StatusException: FAILED_PRECONDITION: The query requires an index. You can create it here: https://console.firebase.google.com/v1/r/project/ ...
직접 파이어베이스로 들어가서 인덱스를 만들라는 링크가 에러 메시지에 같이 첨부가 된다.
파이어 스토어에는 미리 데이터를 정리해주는 인덱싱 과정이 있어서 이를 통해 원하는 데이터를 빠르게 가져올 수 있게 하는데 정렬을 위해 인덱스가 필요하다보니 인덱스를 만들어줘야하는 과정을 직접 처리해주면 된다.


인덱스가 생성되고 코드를 다시 실행하면 에러 없이 정상 실행되는 것을 확인할 수 있다.
// MainFragment.kt
// 메인 화면의 RecyclerView 구성을 위한 리스트
var mainList = mutableListOf<ContentModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentMainBinding = FragmentMainBinding.inflate(inflater)
contentActivity = activity as ContentActivity
// 게시판 종류값을 담아준다.
contentType = arguments?.getInt("TypeNumber")!!
settingToolbar()
settingRecyclerViewMain()
settingRecyclerViewMainSearch()
settingSearchBar()
gettingMainData()
return fragmentMainBinding.root
}
// 현재 게시판의 데이터를 가져와 메인 화면의 리사이클러뷰를 갱신한다.
fun gettingMainData(){
CoroutineScope(Dispatchers.Main).launch {
// 서버에서 데이터를 가져온다.
mainList = ContentDao.gettingContentList(contentType)
// 리사이클러뷰를 갱신한다.
fragmentMainBinding.recyclerViewMain.adapter?.notifyDataSetChanged()
}
}

가져온 글의 작성자 닉네임을 파악하기 위해 모든 사용자 정보를 가져오는 메서드를 작성한다.
// UserDao.kt
// 모든 사용자의 정보를 가져온다.
suspend fun getUserAll():MutableList<UserModel>{
// 사용자 정보를 담을 리스트
val userList = mutableListOf<UserModel>()
val job1 = CoroutineScope(Dispatchers.IO).launch {
// 모든 사용자 정보를 가져온다.
val querySnapshot = Firebase.firestore.collection("UserData").get().await()
// 가져온 문서의 수 만큼 반복한다.
querySnapshot.forEach {
// UserModel 객체에 담는다.
val userModel = it.toObject(UserModel::class.java)
// 리스트에 담는다.
userList.add(userModel)
}
}
job1.join()
return userList
}
// MainFragment.kt
// 사용자 정보를 담고 있을 리스트
var userList = mutableListOf<UserModel>()
// 현재 게시판의 데이터를 가져와 메인 화면의 리사이클러뷰를 갱신한다.
fun gettingMainData(){
CoroutineScope(Dispatchers.Main).launch {
// 서버에서 데이터를 가져온다.
mainList = ContentDao.gettingContentList(contentType)
// 사용자 정보를 가져온다.
userList = UserDao.getUserAll()
// 리사이클러뷰를 갱신한다.
fragmentMainBinding.recyclerViewMain.adapter?.notifyDataSetChanged()
}
}
사용자 번호와 작성자 번호가 같은 경우에 닉네임을 출력하도록 한다.
// MainFragment.kt
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
holder.rowMainBinding.textViewRowMainSubject.text = mainList[position].contentSubject
// 사용자의 수 만큼 반복한다.
userList.forEach {
// 사용자 번호와 작성자 번호가 같으면 출력하고 중단한다.
if(it.userIdx == mainList[position].contentWriterIdx){
holder.rowMainBinding.textViewRowMainNickName.text = it.userNickName
return@forEach
}
}
}

파이어베이스의 스토리지는 noSql기반이라 join을 사용할 수 없어서 이런 방식으로 작업할 수 밖에 없다고 한다.
게시글모델에 사용자 닉네임 필드를 추가하는 방법도 고려하면 좋을 것 같다.
// MainFragment.kt
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
holder.rowMainBinding.textViewRowMainSubject.text = mainList[position].contentSubject
// 사용자의 수 만큼 반복한다.
userList.forEach {
// 사용자 번호와 작성자 번호가 같으면 출력하고 중단한다.
if(it.userIdx == mainList[position].contentWriterIdx){
holder.rowMainBinding.textViewRowMainNickName.text = it.userNickName
return@forEach
}
}
// 항목을 눌렀을 때 동작할 리스너를 연결해준다.
holder.rowMainBinding.root.setOnClickListener {
// 필요한 데이터를 담아준다.
val readBundle = Bundle()
readBundle.putInt("contentIdx", mainList[position].contentIdx)
// 글 읽는 화면으로 이동한다.
contentActivity.replaceFragment(ContentFragmentName.READ_CONTENT_FRAGMENT, true, true, readBundle)
}
}
