커뮤니티 앱 프로젝트
- 블로그를 활용한 커뮤니티 앱 프로젝트입니다.
- 22.08.02. ~ 22.09.21.
- 코틀린으로 주요 기능을 구현합니다.
- Jetpack과 Firebase를 사용합니다.
- Kotlin
- Jetpack
- Firebase
- 1.1. 로그인/로그아웃
- 유효성 검사 후 로그인합니다.
- 비회원으로 로그인하면 회원 가입 절차 없이 로그인하는 대신, 한 번 로그아웃하면 같은 계정으로 다시 로그인할 수 없습니다.
- 로그아웃하면 IntroActivity로 이동합니다.
// FBAuth.kt
// 현재 사용자 uid 받아옴
fun getUid(): String {
// .getInstance() -> 데이터베이스에 접근할 수 있는 진입점
auth = FirebaseAuth.getInstance()
// 현재 사용자 uid를 문자열로 반환
return auth.currentUser?.uid.toString()
}
// 현재 시간 받아옴
fun getTime(): String {
// 캘린더 인스턴스 생성
val currentDateTime = Calendar.getInstance().time
// SimpleDateFormat() -> 사용자가 임의로 표기 형식 지정 가능
// Locale.KOREA -> 지역설정 한국
val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.KOREA).format(currentDateTime)
// 원하는 포맷 및 한국어로 시간 반환
return dateFormat
}
// LoginActivity.kt
// 로그인 버튼 클릭하면
binding.loginBtn.setOnClickListener {
// 로그인 조건 확인
var emailCheck = false
var pwCheck = false
// 이메일, 비밀번호
val emailTxt = binding.email.text.toString()
val pwTxt = binding.pw.text.toString()
// 이메일 정규식
val emailPattern = Patterns.EMAIL_ADDRESS
// 이메일 검사
if(emailTxt.isEmpty()) {
emailCheck = false
binding.emailArea.error = "이메일주소를 입력하세요"
} else if(emailPattern.matcher(emailTxt).matches()) {
emailCheck = true
binding.emailArea.error = null
} else {
emailCheck = false
binding.emailArea.error = "올바른 이메일이 아닙니다"
}
// 비밀번호 검사
if(pwTxt.isEmpty()) {
pwCheck = false
binding.pwArea.error = "비밀번호를 입력하세요"
} else if (pwTxt.length<6) {
pwCheck = false
binding.pwArea.error = "최소 6자 이상 입력하세요"
} else if (pwTxt.length>20) {
pwCheck = false
binding.pwArea.error = "20자 이하로 입력하세요"
} else {
pwCheck = true
binding.pwArea.error = null
}
// 로그인 조건을 모두 만족하면
if(emailCheck and pwCheck) {
// 로그인
auth.signInWithEmailAndPassword(emailTxt, pwTxt)
.addOnCompleteListener(this) { task ->
// 성공하면
if (task.isSuccessful) {
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(this, MainActivity::class.java)
// 메인 액티비티 시작
startActivity(intent)
// 로그인 액티비티 종료
finish()
// 실패하면
} else {
// 메시지 띄움
Toast.makeText(this, "이메일과 비밀번호를 다시 확인하세요", Toast.LENGTH_LONG).show()
}
}
// 조건 불만족하면
} else {
// 메시지 띄움
Toast.makeText(this, "이메일과 비밀번호를 다시 확인하세요", Toast.LENGTH_LONG).show()
}
}
// IntroActivity.kt
// 비회원 버튼 클릭하면
binding.guestBtn.setOnClickListener {
// 익명으로 로그인
auth.signInAnonymously()
.addOnCompleteListener(this) { task ->
// 성공하면
if (task.isSuccessful) {
// 메인 액티비티 시작
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
// 인트로 액티비티 종료
finish()
// 실패하면
} else {
// 토스트 메시지 띄움
Toast.makeText(this, "비회원 로그인에 실패했습니다", Toast.LENGTH_LONG).show()
}
}
}
// MainActivity.kt
// 로그아웃 버튼
alertDialog.findViewById<ConstraintLayout>(R.id.logout)?.setOnClickListener {
// 로그아웃 실행
Firebase.auth.signOut()
// 로그아웃 확인 메시지지
Toast.makeText(this, "로그아웃 되었습니다", Toast.LENGTH_SHORT).show()
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(this, IntroActivity::class.java)
// 인트로 액티비티 시작
startActivity(intent)
// 메인 액티비티 종료
finish()
}
- 1.2. 회원가입
- 유효성 검사 후 가입합니다.
// JoinActivity.kt
// 회원가입 버튼 클릭하면
binding.joinBtn.setOnClickListener {
// 회원가입 조건 확인
var emailCheck = false
var pwCheck = false
var pw2Check = false
// 이메일, 비밀번호, 비밀번호 확인
val emailTxt = binding.email.text.toString()
val pwTxt = binding.pw.text.toString()
val pw2Txt = binding.pw2.text.toString()
// 이메일 정규식
val emailPattern = Patterns.EMAIL_ADDRESS
// 이메일 검사
if(emailTxt.isEmpty()) {
emailCheck = false
binding.emailArea.error = "이메일주소를 입력하세요"
} else if(emailPattern.matcher(emailTxt).matches()) {
emailCheck = true
binding.emailArea.error = null
} else {
emailCheck = false
binding.emailArea.error = "올바른 이메일이 아닙니다"
}
// 비밀번호 검사
if(pwTxt.isEmpty()) {
pwCheck = false
binding.pwArea.error = "비밀번호를 입력하세요"
} else if (pwTxt.length<6) {
pwCheck = false
binding.pwArea.error = "최소 6자 이상 입력하세요"
} else if (pwTxt.length>20) {
pwCheck = false
binding.pwArea.error = "20자 이하로 입력하세요"
} else {
pwCheck = true
binding.pwArea.error = null
}
// 공란 검사
if(emailTxt.isEmpty() || pwTxt.isEmpty() || pw2Txt.isEmpty()) {
emailCheck = false
pwCheck = false
pw2Check = false
}
// 비밀번호 일치 검사
if(pwTxt == pw2Txt && pwTxt.isNotEmpty() && pw2Txt.isNotEmpty()) {
pw2Check = true
binding.pw2Area.error = null
} else if(pwTxt == pw2Txt && pwTxt.isEmpty() && pw2Txt.isEmpty()) {
pw2Check = false
binding.pw2Area.error = "비밀번호를 한 번 더 입력하세요"
} else {
pw2Check = false
binding.pw2Area.error = "비밀번호가 일치하지 않습니다"
}
// 회원가입 조건을 모두 만족하면
if (emailCheck and pwCheck and pw2Check) {
// 회원가입
auth.createUserWithEmailAndPassword(emailTxt, pwTxt)
.addOnCompleteListener(this) { task ->
// 성공하면
if (task.isSuccessful) {
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(this, MainActivity::class.java)
// 메인 액티비티 시작
startActivity(intent)
// 조인 액티비티 종료
finish()
// 조건 만족해도
} else {
// 가입 불가능한 경우가 있음
Toast.makeText(this, "회원가입에 실패했습니다", Toast.LENGTH_LONG).show()
}
}
// 조건을 만족하지 못하면
} else {
// 가입 불가
Toast.makeText(this, "회원가입에 실패했습니다", Toast.LENGTH_LONG).show()
}
}
-
2.1. 블로그 탭
- 컨텐츠 등록시 Glide로 썸네일을 지정합니다.
- 하얀 하트 아이콘을 클릭하면 하트가 주황색으로 바뀌며 컨텐츠가 북마크 탭에 추가됩니다.
- 주황 하트 아이콘을 클릭하면 하트가 하얀색으로 바뀌며 컨텐츠가 북마크 탭에서 삭제됩니다.
- 컨텐츠를 카테고리별로 분류해 웹뷰로 보여줍니다.
- 뒤로가기 버튼을 두 번 클릭하면 웹뷰 창을 닫습니다.
-
2.2. 깃허브 탭
- shinyelee 계정의 깃허브 홈페이지를 보여줍니다.
- 화면 하단 내비게이션 바를 통해 다른 탭으로 이동합니다.
// FBRef.kt
// 블로그 카테고리
val androidStudio = database.getReference("android_studio")
val kotlinSyntax = database.getReference("kotlin_syntax")
val errorWarning = database.getReference("error_warning")
val vcsGithub = database.getReference("vcs_github")
val webInternet = database.getReference("web_internet")
// 북마크 목록
val bookmarkRef = database.getReference("bookmark_list")
// 게시글
val boardRef = database.getReference("board")
// 댓글
val commentRef = database.getReference("comment")
// ContentsModel.kt
// 블로그 컨텐츠에 대한 정보를 데이터 모델 형태로 묶음
data class ContentsModel (
// 블로그 컨텐츠의 제목
var title : String = "",
// 블로그 컨텐츠의 썸네일 이미지 url
var imageUrl : String = "",
// 블로그 컨텐츠의 본문 url
var webUrl : String = ""
)
// ContentsRVAdapter.kt
// 뷰홀더 객체 생성 및 초기화
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContentsRVAdapter.Viewholder {
// 레이아웃 인플레이터 -> 리사이클러뷰에서 뷰홀더 만들 때 반복적으로 사용
val v = LayoutInflater.from(parent.context).inflate(R.layout.contents_rv_item, parent, false)
// 아직 데이터는 들어가있지 않은 껍데기
return Viewholder(v)
}
// 뷰홀더 객체와 데이터를 연결
override fun onBindViewHolder(holder: ContentsRVAdapter.Viewholder, position: Int) {
// 껍데기(뷰홀더의 레이아웃)에 출력할 내용물(아이템 목록, 아이템의 키 목록)을 넣어줌
holder.bindItems(items[position], keyList[position])
}
// 아이템들의 총 개수 반환
override fun getItemCount(): Int = items.size
// 각 아이템에 데이터 넣어줌
inner class Viewholder(itemView : View) : RecyclerView.ViewHolder(itemView) {
// 데이터 매핑(아이템, 아이템의 키)
fun bindItems(item: ContentsModel, key: String) {
// 리사이클러뷰는 setOnItemClickListener 없음 -> 개발자가 직접 구현해야 함
// 아이템뷰(아이템 영역)를 클릭하면
itemView.setOnClickListener {
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(context, ContentsShowActivity::class.java)
// 해당 아이템의 본문 url을 전달
intent.putExtra("url", item.webUrl)
// 컨텐츠쇼 액티비티 시작(웹뷰)
itemView.context.startActivity(intent)
}
// 아이템의 제목 -> titleArea에 넣음
contentsTitle.text = item.title
// 아이템의 썸네일 -> 글라이드로 썸네일 이미지의 url을 imageViewArea에 넣음
Glide.with(context)
.load(item.imageUrl)
.into(imageViewArea)
}
}
// ContentsListActivity.kt
// 컨텐츠모델 형식의 아이템(=컨텐츠=제목+썸네일+본문) 목록
val items = ArrayList<ContentsModel>()
// 각 아이템의 키(=아이디) 목록 -> 북마크에 필요
val keyList = ArrayList<String>()
// 리사이클러뷰 어댑터 연결(컨텍스트, 아이템 목록, 키 목록, 북마크 아이디 목록)
rvAdapter = ContentsRVAdapter(baseContext, items, keyList, bookmarkIdList)
// 파이어베이스
val database = Firebase.database
// 카테고리
val category = intent.getStringExtra("category")
// .getReference() -> 데이터베이스의 루트 폴더 주소 값을 반환
// 카테고리에 해당하는 데이터를 파이어베이스에서 가져옴
when (category) {
"android_studio" -> {
myRef = database.getReference("android_studio")
}
"kotlin_syntax" -> {
myRef = database.getReference("kotlin_syntax")
}
"error_warning" -> {
myRef = database.getReference("error_warning")
}
"vcs_github" -> {
myRef = database.getReference("vcs_github")
}
"web_internet" -> {
myRef = database.getReference("web_internet")
}
}
// 데이터베이스에서 컨텐츠의 세부정보를 검색
val postListener = object : ValueEventListener {
// 데이터 스냅샷
@SuppressLint("NotifyDataSetChanged")
override fun onDataChange(dataSnapshot: DataSnapshot) {
// 데이터 스냅샷 내 데이터모델 형식으로 저장된
for(dataModel in dataSnapshot.children) {
// 아이템을 받아
val item = dataModel.getValue(ContentsModel::class.java)
// 아이템 목록에 넣음
items.add(item!!)
// 키 값은 아이템 키 목록에 넣음
keyList.add(dataModel.key.toString())
}
// 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
rvAdapter.notifyDataSetChanged()
}
// 오류 나면
override fun onCancelled(databaseError: DatabaseError) {
// 로그
Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
}
}
// 파이어베이스 내 데이터의 변화(추가)를 알려줌
myRef.addValueEventListener(postListener)
// 리사이클러뷰 어댑터 연결
val rv : RecyclerView = binding.rv
rv.adapter = rvAdapter
// 그리드 레이아웃 매니저 -> 아이템을 격자 형태로 배치(2열)
rv.layoutManager = GridLayoutManager(this, 2)
// 북마크 정보를 가져옴
getBookmarkData()
// 뒤로가기 버튼 -> 컨텐츠리스트 액티비티 종료
binding.backBtn.setOnClickListener {
finish()
}
// ContentsShowActivity.kt
// 해당 아이템(컨텐츠) 본문의 url을 얻어와서
val getUrl = intent.getStringExtra("url")
// 웹뷰에 넣음
binding.webView.loadUrl(getUrl.toString())
// BlogFragment.kt
// 안드로이드 아이콘 클릭하면
binding.androidIcon.setOnClickListener {
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(context, ContentsListActivity::class.java)
// android_studio 카테고리로 데이터 넘겨줌
intent.putExtra("category", "android_studio")
// 컨텐츠리스트 액티비티 시작
startActivity(intent)
}
// 코틀린 아이콘 -> kotlin_syntax
binding.kotlinIcon.setOnClickListener {
val intent = Intent(context, ContentsListActivity::class.java)
intent.putExtra("category", "kotlin_syntax")
startActivity(intent)
}
// 중략
// WebFragment.kt
// 웹뷰에 깃허브 링크 넣음
binding.webView.loadUrl("https://github.com/shinyelee")
- 3.1. 게시글 CRUD
- 게시글 작성시 Glide로 이미지를 추가합니다.
- 게시글 쓰기/읽기/수정/삭제 가능합니다.
- 내가 쓴 게시글만 수정/삭제 버튼을 보여줍니다.
// BoardModel.kt
// 게시글에 대한 정보를 데이터 모델 형태로 묶음
data class BoardModel (
// 게시글 제목
var title : String = "",
// 게시글 본문
var main : String = "",
// 작성자 uid
var uid : String = "",
// 작성 시간
var time : String = ""
)
// BoardFragment.kt
// 리스트뷰 어댑터 연결(게시글 목록)
boardLVAdapter = BoardLVAdapter(boardList)
// 리스트뷰 어댑터 연결
val lv : ListView = binding.boardLV
lv.adapter = boardLVAdapter
// 모든 게시글 정보를 가져옴
getBoardListData()
// 파이어베이스의 게시글 키를 기반으로 게시글 데이터(=제목+본문+uid+시간) 받아옴
lv.setOnItemClickListener { parent, view, position, id ->
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(context, BoardReadActivity::class.java)
// 글읽기 액티비티로 게시글의 키 값 전달
intent.putExtra("key", boardKeyList[position])
// 글읽기 액티비티 시작
startActivity(intent)
}
// 모든 게시글 정보를 가져옴
private fun getBoardListData() {
// 데이터베이스에서 컨텐츠의 세부정보를 검색
val postListener = object : ValueEventListener {
// 데이터 스냅샷
@SuppressLint("NotifyDataSetChanged")
override fun onDataChange(dataSnapshot: DataSnapshot) {
// 게시글 목록 비움
// -> 저장/삭제 마다 데이터 누적돼 게시글 중복으로 저장되는 것 방지
boardList.clear()
// 데이터 스냅샷 내 데이터모델 형식으로 저장된
for(dataModel in dataSnapshot.children) {
// 로그
Log.d(TAG, "getBoardListData $dataModel")
// 아이템(=게시글)
val item = dataModel.getValue(BoardModel::class.java)
// 게시글 목록에 아이템 넣음
boardList.add(item!!)
// 게시글 키 목록에 문자열 형식으로 변환한 키 넣음
boardKeyList.add(dataModel.key.toString())
}
// getPostData()와 달리 반복문임 -> 아이템'들'
// 게시글 키 목록을 역순으로 출력
boardKeyList.reverse()
Log.d(TAG, "getBoardListData - boardKeyList $boardKeyList")
// 게시글 목록도 역순 출력
boardList.reverse()
// 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
boardLVAdapter.notifyDataSetChanged()
}
// 오류 나면
override fun onCancelled(databaseError: DatabaseError) {
// 로그
Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
}
}
// 파이어베이스 내 데이터의 변화(추가)를 알려줌
FBRef.boardRef.addValueEventListener(postListener)
}
// BoardLVAdapter.kt
// boardList -> 아이템(=게시글=제목+본문+uid+시간) 목록
class BoardLVAdapter(val boardList : MutableList<BoardModel>) : BaseAdapter() {
// 아이템 총 개수 반환
override fun getCount(): Int = boardList.size
// 아이템 반환
override fun getItem(position: Int): Any = boardList[position]
// 아이템의 아이디 반환
override fun getItemId(position: Int): Long = position.toLong()
// 아이템을 표시할 뷰 반환
@SuppressLint("ViewHolder")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view = convertView
// 레이아웃 인플레이터 -> 리사이클러뷰에서 뷰홀더 만들 때 반복적으로 사용
view = LayoutInflater.from(parent?.context).inflate(R.layout.board_lv_item, parent, false)
// 각 아이템뷰의 제목/본문/시간 영역에
val title = view?.findViewById<TextView>(R.id.titleArea)
val time = view?.findViewById<TextView>(R.id.timeArea)
// 제목, 시간 넣음
title!!.text = boardList[position].title
time!!.text = boardList[position].time
// 현재 사용자가 작성한 글만 따로 표시하기 위해
val myBoardBadge = view?.findViewById<TextView>(R.id.myBoardBadge)
// 게시글 작성자의 uid와 현재 사용자의 uid가 일치하면 배지가 보이도록 처리
myBoardBadge?.isVisible = boardList[position].uid.equals(FBAuth.getUid())
// 뷰 반환
return view!!
}
}
// BoardWriteActivity.kt
// 작성한 글을 등록
private fun setBoard(key: String) {
// 게시글의 데이터(제목, 본문, uid, 시간)
val title = binding.titleArea.text.toString()
val main = binding.mainArea.text.toString()
val uid = FBAuth.getUid()
val time = FBAuth.getTime()
// 키 값 하위에 데이터 넣음
FBRef.boardRef
.child(key)
.setValue(BoardModel(title, main, uid, time))
// 카메라 아이콘을 클릭했다면 이미지 업로드
if (isImageUpload == true) {
// 이미지 파일명을 아무렇게나 설정하면 해당 게시글과 매칭하기 어려움
// -> 키 값과 똑같이 설정하면 해결
imageUpload("$key.png")
}
// 카메라 아이콘을 클릭하지 않음 -> 이미지를 업로드하지 않음
// 등록 확인 메시지 띄움
Toast.makeText(this, "게시글이 등록되었습니다", Toast.LENGTH_SHORT).show()
// 글쓰기 액티비티 종료
finish()
}
// 게시글에 이미지 첨부
private fun imageUpload(key: String) {
// Cloud Storage에 파일을 업로드하려면
val storage = Firebase.storage
// -> 우선 파일 이름을 포함하여 파일의 전체 경로를 가리키는 참조를 만듦
val storageRef = storage.reference
// 임의의 게시글 하나와 그 게시글 내 이미지 하나를 쉽게 매칭하려면
// -> DB 내 게시글 키 값과 첨부한 이미지 이름이 똑같으면 됨
val testRef = storageRef.child(key)
// 적절한 참조를 만들었으면
binding.imageArea.isDrawingCacheEnabled = true
binding.imageArea.buildDrawingCache()
val bitmap = (binding.imageArea.drawable as BitmapDrawable).bitmap
val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
val data = baos.toByteArray()
// -> put어쩌고() 메서드를 호출하여 Cloud Storage에 파일을 업로드
var uploadTask = testRef.putBytes(data)
uploadTask.addOnFailureListener {
Log.d(TAG, "imageUpload() failed")
}.addOnSuccessListener { taskSnapshot ->
// 성공
}
}
// startActivityForResult와 세트
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == RESULT_OK && requestCode == 100) {
binding.imageArea.setImageURI(data?.data)
}
}
// BoardReadActivity.kt
// 게시판 프래그먼트에서 게시글의 키 값을 받아옴
key = intent.getStringExtra("key").toString()
// 게시글 키 값을 바탕으로 게시글 하나의 정보를 가져옴
getBoardData(key)
getImageData(key)
getCommentListData(key)
// 게시글 설정 버튼
binding.boardSettingBtn.setOnClickListener {
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(this, BoardEditActivity::class.java)
// 키 값을 바탕으로 게시글 받아옴
intent.putExtra("key", key)
// 글수정 액티비티 시작
startActivity(intent)
}
// 게시글에 첨부된 이미지 정보를 가져옴
private fun getImageData(key: String) {
// 이미지 파일 경로
val storageReference = Firebase.storage.reference.child("$key.png")
// 이미지 넣을 곳
val imgDown = binding.imageArea
// 글라이드로 이미지 다운로드
storageReference.downloadUrl.addOnCompleteListener( { task ->
// 이미지 첨부
if(task.isSuccessful) {
Glide.with(this)
.load(task.result)
.into(imgDown)
// 첨부 이미지 없으면 imageArea 안 보이게 처리
} else {
binding.imageArea.isVisible = false
}
})
}
// 게시글 하나의 정보를 가져옴
private fun getBoardData(key: String) {
// 데이터베이스에서 컨텐츠의 세부정보를 검색
val postListener = object : ValueEventListener {
// 데이터 스냅샷
@SuppressLint("NotifyDataSetChanged")
override fun onDataChange(dataSnapshot: DataSnapshot) {
// 예외 처리
try {
// 데이터 스냅샷 내 데이터모델 형식으로 저장된 아이템(=게시글)
val item = dataSnapshot.getValue(BoardModel::class.java)
// 제목, 시간, 본문 해당 영역에 넣음
binding.titleArea.text = item!!.title
binding.timeArea.text = item.time
binding.mainArea.text = item.main
// 게시글 작성자와 현재 사용자의 uid를 비교해
val writerUid = item.uid
val myUid = FBAuth.getUid()
// 작성자가 사용자면 수정 버튼 보임
binding.boardSettingBtn.isVisible = writerUid.equals(myUid)
// 오류 나면
} catch (e: Exception) {
// 로그
Log.d(TAG, "getBoardData 확인")
}
}
// getBoardListData()와 달리 반복문이 아님 -> '단일' 아이템
// 오류 나면
override fun onCancelled(databaseError: DatabaseError) {
// 로그
Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
}
}
// 파이어베이스 내 데이터의 변화(추가)를 알려줌
FBRef.boardRef.child(key).addValueEventListener(postListener)
}
// BoardEditActivity.kt
// 글읽기 프래그먼트에서 게시글의 키 값을 받아옴
key = intent.getStringExtra("key").toString()
// 키 값을 바탕으로 게시글 하나의 정보를 가져옴
getBoardData(key)
// 키 값을 바탕으로 게시글에 첨부된 이미지 정보를 가져옴
getImageData(key)
// 수정하기 버튼 -> 키 값을 바탕으로 불러온 게시글 수정
binding.boardEditBtn.setOnClickListener { editBoardData(key) }
// 삭제하기 버튼 -> 키 값을 바탕으로 불러온 댓글 삭제
binding.boardDeleteBtn.setOnClickListener { deleteBoardData(key) }
// 게시글을 삭제
private fun deleteBoardData(key: String) {
// 게시글 삭제
FBRef.boardRef.child(key).removeValue()
// 삭제 확인 메시지
Toast.makeText(this, "게시글이 삭제되었습니다", Toast.LENGTH_SHORT).show()
// 게시글수정 액티비티 종료
finish()
}
// 게시글을 수정
private fun editBoardData(key: String) {
// 수정한 값으로 업데이트
FBRef.boardRef.child(key).setValue(BoardModel(
// 제목 및 본문은 직접 수정한 내용으로,
binding.titleArea.text.toString(),
binding.mainArea.text.toString(),
// uid와 시간은 자동 설정됨
FBAuth.getUid(),
FBAuth.getTime()
))
// 수정 확인 메시지
Toast.makeText(this, "게시글이 수정되었습니다", Toast.LENGTH_SHORT).show()
// 글수정 액티비티 종료
finish()
}
// 게시글에 첨부된 이미지 정보를 가져옴
private fun getImageData(key: String) {
// 중복코드 생략
}
// 게시글 하나의 정보를 가져옴
private fun getBoardData(key: String) {
// 중복코드 생략
}
- 3.2. 댓글 CRUD
- 댓글 쓰기/읽기/수정/삭제 가능합니다(22.08.31. 업데이트).
- 내가 쓴 댓글만 수정/삭제 버튼 버튼을 보여줍니다(22.08.26. 업데이트).
// CommentModel.kt
// 게시글에 대한 정보를 데이터 모델 형태로 묶음
data class CommentModel (
// 댓글 내용
var main : String = "",
// 작성자 uid
var uid : String = "",
// 작성 시간
var time : String = ""
)
// BoardReadActivity.kt
// 리스트뷰 어댑터 연결(댓글 목록)
commentLVAdapter = CommentLVAdapter(commentList)
val cLV : ListView = binding.commentLV
cLV.adapter = commentLVAdapter
// 댓글 목록(리스트뷰)
cLV.setOnTouchListener(object : View.OnTouchListener {
// 리스트뷰를 터치했을 때
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
// 스크롤뷰(화면 전체)의 터치 이벤트를 막으면 -> 리스트뷰(댓글 영역)의 스크롤뷰가 작동함
binding.boardReadSV.requestDisallowInterceptTouchEvent(true)
return false
}
})
// 게시판 프래그먼트에서 게시글의 키 값을 받아옴
key = intent.getStringExtra("key").toString()
// 게시글 키 값을 바탕으로 게시글 하나의 정보를 가져옴
getBoardData(key)
getImageData(key)
getCommentListData(key)
// 게시글 설정 버튼
binding.boardSettingBtn.setOnClickListener {
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(this, BoardEditActivity::class.java)
// 키 값을 바탕으로 게시글 받아옴
intent.putExtra("key", key)
// 글수정 액티비티 시작
startActivity(intent)
}
// 파이어베이스의 댓글 키를 기반으로 댓글 데이터(=본문+uid+시간) 받아옴
cLV.setOnItemClickListener { parent, view, position, id ->
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(baseContext, CommentEditActivity::class.java)
// 댓글수정 액티비티로 댓글의 키 값 전달
intent.putExtra("key", key)
// 댓글수정 액티비티로 댓글의 키 값 전달
intent.putExtra("commentKey", commentKeyList[position])
// 댓글수정 액티비티 시작
startActivity(intent)
}
// 댓글쓰기 버튼
binding.commentBtn.setOnClickListener {
// -> 작성한 댓글을 등록
setComment(key)
}
}
// 댓글 목록 정보 가져옴
private fun getCommentListData(key: String) {
// 데이터베이스에서 컨텐츠의 세부정보를 검색
val postListener = object : ValueEventListener {
// 데이터 스냅샷
@SuppressLint("NotifyDataSetChanged")
override fun onDataChange(dataSnapshot: DataSnapshot) {
// 댓글 목록 비움
// -> 저장/삭제 마다 데이터 누적돼 게시글 중복으로 저장되는 것 방지
commentList.clear()
// 데이터 스냅샷 내 데이터모델 형식으로 저장된
for(dataModel in dataSnapshot.children) {
// 로그
Log.d(TAG, dataModel.toString())
// 아이템(=댓글)
val item = dataModel.getValue(CommentModel::class.java)
// 댓글 목록에 아이템 넣음
commentList.add(item!!)
// 댓글 키 목록에 문자열 형식으로 변환한 키 넣음
commentKeyList.add(dataModel.key.toString())
}
// 반복문임 -> 아이템'들'
// 댓글 키 목록을 출력
commentKeyList
// 댓글 목록도 출력
commentList
// 댓글 헤더에 댓글 개수 출력
binding.commentCountText.text = commentList.count().toString()
// 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
commentLVAdapter.notifyDataSetChanged()
}
// 오류 나면
override fun onCancelled(databaseError: DatabaseError) {
// 로그
Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
}
}
// 파이어베이스 내 데이터의 변화(추가)를 알려줌
FBRef.commentRef.child(key).addValueEventListener(postListener)
}
// 작성한 댓글을 등록
private fun setComment(key: String) {
// 댓글의 데이터(본문, uid, 시간)
val main = binding.commentMainArea.text.toString()
val uid = FBAuth.getUid()
val time = FBAuth.getTime()
// 키 값 하위에 데이터 넣음
FBRef.commentRef
.child(key)
.push()
.setValue(CommentModel(main, uid, time))
// 등록 확인 메시지 띄움
Toast.makeText(this, "댓글이 등록되었습니다", Toast.LENGTH_SHORT).show()
// 댓글 입력란 비움
binding.commentMainArea.text = null
}
// CommentLVAdapter.kt
// 아이템 총 개수 반환
override fun getCount(): Int = commentList.size
// 아이템 반환
override fun getItem(position: Int): Any = commentList[position]
// 아이템의 아이디 반환
override fun getItemId(position: Int): Long = position.toLong()
// 아이템을 표시할 뷰 반환
@SuppressLint("ViewHolder", "ClickableViewAccessibility")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view = convertView
// 레이아웃 인플레이터 -> 리사이클러뷰에서 뷰홀더 만들 때 반복적으로 사용
view = LayoutInflater.from(parent?.context).inflate(R.layout.comment_lv_item, parent, false)
// 각 아이템뷰의 본문, 시간 영역에
val commentMain = view?.findViewById<TextView>(R.id.commentMainArea)
val commentTime = view?.findViewById<TextView>(R.id.commentTimeArea)
// 본문, 시간 넣음
commentMain!!.text = commentList[position].main
commentTime!!.text = commentList[position].time
// 댓글 작성자의 uid와 현재 사용자의 uid가 일치하면 댓글 배지가 보이도록 처리
val myCommentBadge = view?.findViewById<TextView>(R.id.myCommentBadge)
myCommentBadge?.isVisible = commentList[position].uid.equals(FBAuth.getUid())
// 댓글 세팅 버튼도 동일함
val commentSettingBtn = view?.findViewById<ImageView>(R.id.commentSettingBtn)
commentSettingBtn?.isVisible = commentList[position].uid.equals(FBAuth.getUid())
// 댓글 작성자의 uid와 현재 사용자의 uid가 다르면 터치 막음
val commentLVItem = view?.findViewById<LinearLayout>(R.id.commentLVItemLayout)
if(commentList[position].uid != FBAuth.getUid()) {
commentLVItem?.setOnTouchListener(View.OnTouchListener { v, event -> true })
}
// 뷰 반환
return view!!
}
// CommentEditActivity.kt
// 게시판 프래그먼트에서 게시글의 키 값을 받아옴
key = intent.getStringExtra("key").toString()
// 글읽기 액티비티에서 댓글의 키 값을 받아옴
commentKey = intent.getStringExtra("commentKey").toString()
// 댓글 키 값을 바탕으로 댓글 하나의 정보를 가져옴
getCommentData(key, commentKey)
// 수정하기 버튼 -> 키 값을 바탕으로 불러온 댓글 수정
binding.commentEditBtn.setOnClickListener { editCommentData(key, commentKey) }
// 삭제하기 버튼 -> 키 값을 바탕으로 불러온 댓글 삭제
binding.commentDeleteBtn.setOnClickListener { deleteCommentData(key, commentKey) }
// 댓글을 삭제
private fun deleteCommentData(key: String, commentKey: String) {
// 댓글 삭제
FBRef.commentRef.child(key).child(commentKey).removeValue()
// 삭제 확인 메시지
Toast.makeText(this, "댓글이 삭제되었습니다", Toast.LENGTH_SHORT).show()
// 댓글수정 액티비티 종료
finish()
}
// 댓글을 수정
private fun editCommentData(key: String, commentKey: String) {
// 수정한 값으로 업데이트
FBRef.commentRef.child(key).child(commentKey).setValue(CommentModel(
// 제목 및 본문은 직접 수정한 내용으로,
binding.commentMainArea.text.toString(),
// uid와 시간은 자동 설정됨
FBAuth.getUid(),
FBAuth.getTime()
))
// 수정 확인 메시지
Toast.makeText(this, "댓글이 수정되었습니다", Toast.LENGTH_SHORT).show()
// 댓글수정 액티비티 종료
finish()
}
// 댓글 하나의 정보를 가져옴
private fun getCommentData(key: String, commentKey: String) {
// 데이터베이스에서 컨텐츠의 세부정보를 검색
val postListener = object : ValueEventListener {
// 데이터 스냅샷
@SuppressLint("NotifyDataSetChanged")
override fun onDataChange(dataSnapshot: DataSnapshot) {
// 예외 처리
try {
// 데이터 스냅샷 내 데이터모델 형식으로 저장된 아이템(=게시글)
val item = dataSnapshot.getValue(CommentModel::class.java)
// 본문 해당 영역에 넣음(작성자 및 시간은 직접 수정하지 않음)
binding.commentMainArea.setText(item?.main)
// textView -> .text
// editText -> .setText(집어넣을 데이터)
// 오류 나면
} catch (e: Exception) {
// 로그
Log.e(TAG, "getBoardData 확인")
}
}
// getCommentListData()와 달리 반복문이 아님 -> '단일' 아이템
// 오류 나면
override fun onCancelled(databaseError: DatabaseError) {
// 로그
Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
}
}
// 파이어베이스 내 데이터의 변화(추가)를 알려줌
FBRef.commentRef.child(key).child(commentKey).addValueEventListener(postListener)
}
- 하얀 하트 아이콘을 클릭하면 하트가 주황색으로 바뀌며 컨텐츠가 북마크 탭에 추가됩니다.
- 주황 하트 아이콘을 클릭하면 하트가 하얀색으로 바뀌며 컨텐츠가 북마크 탭에서 삭제됩니다.
// ContentsRVAdapter.kt
// 각 아이템뷰의 제목/썸네일/북마크(하트) 영역
val contentsTitle = itemView.findViewById<TextView>(R.id.titleArea)
val imageViewArea = itemView.findViewById<ImageView>(R.id.imageArea)
val bookmarkArea = itemView.findViewById<ImageView>(R.id.bookmarkArea)
// -> 북마크아이디리스트(=북마크된 아이템의 키 목록)에 화면에 표시된 아이템의 키 정보가 포함되면
if(bookmarkIdList.contains(key)) {
// 해당 아이템의 하트 -> 주황색
bookmarkArea.setImageResource(R.drawable.bookmark56)
// 포함되지 않으면
} else {
// 하트 -> 하얀색
bookmarkArea.setImageResource(R.drawable.bookmark56w)
}
// 하트 클릭하면
bookmarkArea.setOnClickListener {
// bookmark_list 하위에 사용자 uid별로 나눠 게시글의 키 값을 저장
// 이미 북마크 된 상태
if(bookmarkIdList.contains(key)) {
// -> 북마크 삭제
FBRef.bookmarkRef
.child(FBAuth.getUid())
.child(key)
.removeValue()
// 아직 북마크 안 된 상태
} else {
// -> 북마크 저장
FBRef.bookmarkRef
.child(FBAuth.getUid())
.child(key)
.setValue(BookmarkModel(true))
}
}
// ContentsListActiviti.kt
// 북마크 정보를 가져옴
private fun getBookmarkData() {
// 데이터베이스에서 컨텐츠의 세부정보를 검색
val postListener = object : ValueEventListener {
// 데이터 스냅샷
@SuppressLint("NotifyDataSetChanged")
override fun onDataChange(dataSnapshot: DataSnapshot) {
// .clear() -> 북마크 아이디 목록 비움(저장/삭제 마다 데이터 누적되는 것 방지)
bookmarkIdList.clear()
// 데이터 스냅샷 내 데이터모델 형식으로 저장된
for(dataModel in dataSnapshot.children) {
// 북마크 아이디 목록에 아이템의 키 값을 넣음
bookmarkIdList.add(dataModel.key.toString())
}
// 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
rvAdapter.notifyDataSetChanged()
}
// 오류 나면
override fun onCancelled(databaseError: DatabaseError) {
// 로그
Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
}
}
// 파이어베이스 내 데이터 변화(추가)를 알려줌
FBRef.bookmarkRef.child(FBAuth.getUid()).addValueEventListener(postListener)
}
// BookmarkModel.kt
// 컨텐츠의 북마크 여부를 데이터 모델 형태로 묶음
data class BookmarkModel (
// boolean 값이 true -> 북마크 된 컨텐츠
val bookmarkOn : Boolean? = null
)
// BookmarkRVAdapter.kt
// 뷰홀더 객체 생성 및 초기화
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkRVAdapter.Viewholder {
// 레이아웃 인플레이터 -> 리사이클러뷰에서 뷰홀더 만들 때 반복적으로 사용
val v = LayoutInflater.from(parent.context).inflate(R.layout.contents_rv_item, parent, false)
// 아직 데이터는 들어가있지 않은 껍데기
return Viewholder(v)
}
// 뷰홀더 객체와 데이터를 연결
override fun onBindViewHolder(holder: BookmarkRVAdapter.Viewholder, position: Int) {
// 껍데기(뷰홀더의 레이아웃)에 출력할 내용물(아이템 목록, 아이템의 키 목록)을 넣어줌
holder.bindItems(items[position], keyList[position])
}
// 아이템들의 총 개수 반환
override fun getItemCount(): Int = items.size
// 각 아이템에 데이터 넣어줌
inner class Viewholder(itemView : View) : RecyclerView.ViewHolder(itemView) {
// 데이터 매핑(아이템, 아이템의 키)
fun bindItems(item: ContentsModel, key: String) {
// 리사이클러뷰는 setOnItemClickListener 없음 -> 개발자가 직접 구현해야 함
// 아이템뷰(아이템 영역)를 클릭하면
itemView.setOnClickListener {
// 명시적 인텐트 -> 다른 액티비티 호출
val intent = Intent(context, ContentsShowActivity::class.java)
// 해당 아이템의 본문 url을 전달
intent.putExtra("url", item.webUrl)
// 컨텐츠쇼 액티비티 시작(웹뷰)
itemView.context.startActivity(intent)
}
// 각 아이템뷰의 제목/썸네일/북마크(하트) 영역
val contentsTitle = itemView.findViewById<TextView>(R.id.titleArea)
val imageViewArea = itemView.findViewById<ImageView>(R.id.imageArea)
val bookmarkArea = itemView.findViewById<ImageView>(R.id.bookmarkArea)
// -> 북마크아이디리스트(=북마크된 아이템의 키 목록)에 화면에 표시된 아이템의 키 정보가 포함되면
if(bookmarkIdList.contains(key)) {
// 해당 아이템의 하트 -> 주황색
bookmarkArea.setImageResource(R.drawable.bookmark56)
// 포함되지 않으면
} else {
// 하트 -> 하얀색
bookmarkArea.setImageResource(R.drawable.bookmark56w)
}
// 하트 클릭하면
bookmarkArea.setOnClickListener {
// bookmark_list 하위에 사용자 uid별로 나눠 게시글의 키 값을 저장
// 이미 북마크 된 상태
if(bookmarkIdList.contains(key)) {
// -> 북마크 삭제
FBRef.bookmarkRef
.child(FBAuth.getUid())
.child(key)
.removeValue()
// 아직 북마크 안 된 상태
} else {
// -> 북마크 저장
FBRef.bookmarkRef
.child(FBAuth.getUid())
.child(key)
.setValue(BookmarkModel(true))
}
}
// 아이템의 제목 -> titleArea에 넣음
contentsTitle.text = item.title
// 아이템의 썸네일 -> 글라이드로 썸네일 이미지의 url을 imageViewArea에 넣음
Glide.with(context)
.load(item.imageUrl)
.into(imageViewArea)
}
}
// BookmarkFragment.kt
// 블로그 탭의 모든 컨텐츠 가져옴
private fun getBlogData() {
// 데이터베이스에서 게시물의 세부정보를 검색
val postListener = object : ValueEventListener {
// 데이터 스냅샷
@SuppressLint("NotifyDataSetChanged")
override fun onDataChange(dataSnapshot: DataSnapshot) {
// 데이터 스냅샷 내 데이터모델 형식으로 저장된
for(dataModel in dataSnapshot.children) {
// 모든 컨텐츠(키, 본문 url, 썸네일 url, 제목) 출력
Log.d(TAG, dataModel.toString())
// 아이템을 받아
val item = dataModel.getValue(ContentsModel::class.java)
// 북마크 아이디 목록에 키가 포함(북마크 저장)된 경우만
if(bookmarkIdList.contains(dataModel.key.toString())) {
// 아이템을 아이템 목록에 넣음
items.add(item!!)
// 키 값은 아이템 키 목록에 넣음
keyList.add(dataModel.key.toString())
}
}
// 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
rvAdapter.notifyDataSetChanged()
}
// 오류 나면
override fun onCancelled(databaseError: DatabaseError) {
// 로그
Log.w(ContentValues.TAG, "loadPost:onCancelled", databaseError.toException())
}
}
// 파이어베이스 내 카테고리별 컨텐츠 데이터의 변화(추가)를 알려줌
FBRef.androidStudio.addValueEventListener(postListener)
FBRef.kotlinSyntax.addValueEventListener(postListener)
FBRef.errorWarning.addValueEventListener(postListener)
FBRef.vcsGithub.addValueEventListener(postListener)
FBRef.webInternet.addValueEventListener(postListener)
}
- DB 전체
- android_studio, bookmark_list, error_warning, kotlin_syntax, vcs_github, web_internet(카테고리별 컨텐츠)
- board(게시판)
- comment(댓글)
- bookmark_list(북마크)
- 댓글 수정/삭제 불가.
- 댓글 개수 미구현.
- 스크롤뷰 내 리스트뷰 삽입으로 인한 댓글 출력 오류.
- 댓글 수정/삭제 기능 추가(22.08.31. 업데이트).
- 게시글 내부에 댓글 개수 표시(22.09.21. 업데이트). 추후 게시글 목록에서도 댓글 개수가 보이도록 할 것.
- 댓글 스크롤 오류 수정(22.09.28. 업데이트).
이 프로젝트는 MIT 라이센스에 따라 라이센스가 부여됩니다. 자세한 내용은 LICENSE.md 파일을 참조하십시오.