Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[강원대 Android_주민철] 2주차 과제 2단계 #65

Open
wants to merge 31 commits into
base: joominchul
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3b598dd
Docs[README.md]: 생성 리드미
joominchul Jul 5, 2024
2c41560
Design: 생성 검색 화면 UI
joominchul Jul 5, 2024
4f9066a
Feat: 생성 데이터베이스
joominchul Jul 5, 2024
96926be
Feat: 생성 addPlace
joominchul Jul 5, 2024
6080cee
Feat[PlaceDbHelper.kt]: 생성 existPlace
joominchul Jul 5, 2024
9a75a01
Feat[PlaceDbHelper.kt]: 변경 addPlace
joominchul Jul 5, 2024
8ab4b19
Feat[PlaceDbHelper.kt]: 추가 searchPlaceName
joominchul Jul 5, 2024
a2f41fa
Feat[PlaceDbHelper.kt]: 추가 existData
joominchul Jul 5, 2024
175ca91
Feat[]: 추가 검색에 사용될 데이터
joominchul Jul 5, 2024
c3c7e22
Docs[README.md]: 추가 2단계
joominchul Jul 5, 2024
76521a0
Docs[README.md]: 추가 이미지 출처
joominchul Jul 5, 2024
c4505c0
Feat[]: 추가 검색 결과 목록 출력
joominchul Jul 6, 2024
217b4d8
Feat[MainActivity.kt]: 추가 X를 눌러 입력 검색어 삭제
joominchul Jul 6, 2024
119090f
Fix[PlaceDbHelper.kt]: 변경 existPlace
joominchul Jul 6, 2024
5aaa892
Refactor[]: Place DB 이름을 상수로 변경
joominchul Jul 6, 2024
790679a
Rename[PlaceAdapter]: 변경 이름
joominchul Jul 6, 2024
c2baa69
Refactor[PlaceDbHelper]: 변경 existData
joominchul Jul 6, 2024
78eacb1
Feat[]: 추가 검색 결과 목록 기능
joominchul Jul 6, 2024
568b0f9
Feat[SearchWordDbHelper]: 추가 검색어 x 눌러서 삭제 기능
joominchul Jul 6, 2024
204ffcb
Feat[]: 추가 검색어 재실행 유지
joominchul Jul 6, 2024
08a7a27
Refactor[]: 변경 검색어 검색 인식 방법
joominchul Jul 7, 2024
0890b9e
Refactor[]: 변경 UI세팅 함수명
joominchul Jul 7, 2024
2bfa781
Comment[MainActivity]: 삭제 테스트용 로그
joominchul Jul 7, 2024
b868cb3
Refactor[MainActivity]: 변경 warning 부분
joominchul Jul 7, 2024
326f711
Refactor[]: 변경 place와 word 라이브데이터 타입
joominchul Jul 7, 2024
3dcef65
Refactor[]: 삭제 필요 없는 import, 변경 WordfromPlace 함수 이름
joominchul Jul 7, 2024
ac38146
Refactor[]: 변경 _place, _searchWords 타입
joominchul Jul 7, 2024
8d5bdd7
Refactor[]: 추가 DB Version 상수
joominchul Jul 7, 2024
083eeb6
Refactor[]: 추가 createTable
joominchul Jul 7, 2024
de4f9f0
Refactor[]: 변경 어댑터 클릭리스너
joominchul Jul 7, 2024
c0f5b5b
Refactor[]: 변경 어댑터에 ListAdapter 사용
joominchul Jul 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
# android-map-keyword
# 1단계

## 기능 요구 사항
- 카카오맵 클론 코딩을 위한 시작입니다.
- 검색어 입력 및 검색 결과를 표시할 기본 레이아웃을 구현한다.
- 검색에 사용될 데이터를 로컬 데이터베이스에 생성한다.

## 프로그래밍 요구 사항
- 검색 데이터는 저장은 SQLite를 사용한다.
- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.

# 2단계

## 기능 요구 사항
- 검색어를 입력하면 검색 결과 목록이 표시된다.
- 검색 결과 목록은 세로 스크롤이 된다.
- 입력한 검색어는 X를 눌러서 삭제할 수 있다.
- 검색 결과 목록에서 하나의 항목을 선택할 수 있다.
- 선택된 항목은 검색어 저장 목록에 추가된다.
- 목록에 있는 검색 결과를 다시 선택하면 기존 저장 목록에서 지워지고 가장 끝에 다시 추가된다.
- 저장된 검색어 목록은 가로 스크롤이 된다.
- 저장된 검색어는 X를 눌러서 삭제할 수 있다.
- 저장된 검색어는 앱을 재실행하여도 유지된다.

## 프로그래밍 요구 사항
- 검색 결과 목록은 리사이클러뷰를 사용한다.
- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다.

# 3단계

## 기능 요구 사항
- 저장된 검색어 목록은 RecyclerView로 구현하고 검색 결과는 ListView로 구현해 보세요.

## 프로그래밍 요구 사항
- ListView 사용 시 재사용 가능한 뷰를 최대한 재사용할 수 있도록 구현한다.

## 이미지 출처
- 위치 마커 아이콘 제작자: bsd - Flaticon
- 문자 x 아이콘 제작자: Freepik - Flaticon
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ android {
}

dependencies {

val lifecycle_version = "2.8.3"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
Expand Down
78 changes: 78 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,89 @@
package campus.tech.kakao.map

import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

private lateinit var model: MainViewModel
private lateinit var search:EditText
private lateinit var clear: TextView
private lateinit var noResult: TextView
private lateinit var searchResult: RecyclerView
private lateinit var searchWordResult: RecyclerView
private lateinit var placeAdapter: PlaceAdapter
private lateinit var wordAdapter: WordAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
searchResult.layoutManager = LinearLayoutManager(this)
searchWordResult.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

search.doOnTextChanged { text, start, before, count ->
val query = text.toString()
if (query.isEmpty()){
noResult.visibility = View.VISIBLE
searchResult.visibility = View.GONE
}else{
noResult.visibility = View.GONE
searchResult.visibility = View.VISIBLE
model.search(query)
}
}
model = ViewModelProvider(this)[MainViewModel::class.java]
model.placeList.observe(this, Observer {
if (it.isNullOrEmpty()){
noResult.visibility = View.VISIBLE
searchResult.visibility = View.GONE
}else{
noResult.visibility = View.GONE
searchResult.visibility = View.VISIBLE
placeAdapter = PlaceAdapter(){ Place ->
model.addWord(Place)
}
placeAdapter.submitList(it)
searchResult.adapter = placeAdapter
}
})
model.loadWord()
model.wordList.observe(this, Observer {
if (it.isNullOrEmpty()){
searchWordResult.visibility = View.GONE
}
else{
searchWordResult.visibility = View.VISIBLE
wordAdapter = WordAdapter() { SearchWord ->
model.deleteWord(
SearchWord
)
}
wordAdapter.submitList(it)
searchWordResult.adapter = wordAdapter
}
})

}

fun setupUI(){
search = findViewById(R.id.search)
clear = findViewById(R.id.search_clear)
noResult = findViewById(R.id.no_search_result)
searchResult = findViewById(R.id.search_result_recycler_view)
searchWordResult = findViewById(R.id.search_word_recycler_view)
clear.setOnClickListener {
search.setText("")
}
}


}
43 changes: 43 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package campus.tech.kakao.map

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData

class MainViewModel(application: Application): AndroidViewModel(application) {
private val placeDbHelper = PlaceDbHelper(application)
val placeList: LiveData<List<Place>> get() = placeDbHelper.getPlace()

private val wordDbHelper = SearchWordDbHelper(application)
val wordList: LiveData<List<SearchWord>> get() = wordDbHelper.getSearchWords()

//초기에 데이터 삽입을 위해 1번 사용
fun insertInitData(){
if (!placeDbHelper.existData()){
for(i in 1..10){
placeDbHelper.addPlace(Place("카페 $i", "남양주 $i", "카페"))
placeDbHelper.addPlace(Place("약국 $i", "남양주 $i", "약국"))
}
}

}

fun search(query: String) {
placeDbHelper.searchPlaceName(query)
}

fun addWord(place: Place){
wordDbHelper.addWord(wordfromPlace(place))
}

private fun wordfromPlace(place: Place):SearchWord{
return SearchWord(place.name, place.address, place.type)
}
fun deleteWord(word: SearchWord){
wordDbHelper.deleteWord(word)
}

fun loadWord(){
wordDbHelper.updateSearchWords()
}
}
3 changes: 3 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/Place.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package campus.tech.kakao.map

data class Place(val name: String, val address: String, val type: String)
56 changes: 56 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/PlaceAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package campus.tech.kakao.map

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

class PlaceAdapter(
val onItemClicked: (Place) -> Unit
): ListAdapter<Place, PlaceAdapter.ViewHolder>(
object : DiffUtil.ItemCallback<Place>(){
override fun areItemsTheSame(oldItem: Place, newItem: Place): Boolean {
return (oldItem.name == newItem.name)
&& (oldItem.address == newItem.address)
&& (oldItem.type == newItem.type)
}

override fun areContentsTheSame(oldItem: Place, newItem: Place): Boolean {
return oldItem == newItem
}

}
) {
private var placeClicked = { position:Int ->
val place:Place = getItem(position)
onItemClicked(place)
}
inner class ViewHolder(
itemView: View
): RecyclerView.ViewHolder(itemView) {
val name:TextView = itemView.findViewById(R.id.name)
val address:TextView = itemView.findViewById(R.id.address)
val type:TextView = itemView.findViewById(R.id.type)
init {
itemView.setOnClickListener {
placeClicked(bindingAdapterPosition)
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.place_item, parent, false))
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val place:Place = getItem(position)
holder.name.text = place.name
holder.address.text = place.address
holder.type.text = place.type
}
}


12 changes: 12 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/PlaceContract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package campus.tech.kakao.map

import android.provider.BaseColumns

object PlaceContract:BaseColumns {
const val DB_NAME = "place.db"
const val DB_VERSION = 2
const val TABLE_NAME = "place"
const val COLUMN_NAME_NAME = "name"
const val COLUMN_NAME_ADDRESS = "address"
const val COLUMN_NAME_TYPE = "type"
}
108 changes: 108 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/PlaceDbHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package campus.tech.kakao.map

import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.lifecycle.MutableLiveData
import campus.tech.kakao.map.PlaceContract.DB_VERSION

class PlaceDbHelper(context: Context):SQLiteOpenHelper(
context, PlaceContract.DB_NAME, null, DB_VERSION) {
private val _place = MutableLiveData<List<Place>>()
override fun onCreate(db: SQLiteDatabase?) {
createTable(db)
}
private fun createTable(db: SQLiteDatabase?) {
db?.execSQL("CREATE TABLE ${PlaceContract.TABLE_NAME} " +
"(${PlaceContract.COLUMN_NAME_NAME} TEXT, " +
"${PlaceContract.COLUMN_NAME_ADDRESS} TEXT, " +
"${PlaceContract.COLUMN_NAME_TYPE} TEXT)")
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL("DROP TABLE IF EXISTS ${PlaceContract.TABLE_NAME}")
createTable(db)
}

fun getPlace(): MutableLiveData<List<Place>> {
return _place
}

fun addPlace(place: Place) {
val db = writableDatabase
if (!existPlace(place, db)){
val values = ContentValues()
values.put(PlaceContract.COLUMN_NAME_NAME, place.name)
values.put(PlaceContract.COLUMN_NAME_ADDRESS, place.address)
values.put(PlaceContract.COLUMN_NAME_TYPE, place.type)
db.insert(PlaceContract.TABLE_NAME, null, values)
db.close()
}
}

fun existData(): Boolean{
val db = readableDatabase
val cursor = db.query(
PlaceContract.TABLE_NAME,
arrayOf(PlaceContract.COLUMN_NAME_NAME),
null,
null,
null,
null,
null
)
val result = cursor.moveToFirst()
cursor.close()
return result
}

fun existPlace(place: Place, db: SQLiteDatabase): Boolean{
val selection = "${PlaceContract.COLUMN_NAME_NAME} = ? AND " +
"${PlaceContract.COLUMN_NAME_ADDRESS} = ? AND " +
"${PlaceContract.COLUMN_NAME_TYPE} = ?"
val cursor = db.query(
PlaceContract.TABLE_NAME,
arrayOf(PlaceContract.COLUMN_NAME_NAME),
selection,
arrayOf(place.name, place.address, place.type),
null,
null,
"${PlaceContract.COLUMN_NAME_NAME} DESC"
)

val result = cursor.moveToFirst()
cursor.close()
return if (result) true else false
}

fun searchPlaceName(name: String){
val resultList = mutableListOf<Place>()
val searchResult = "%${name}%"
val cursor = readableDatabase.query(
PlaceContract.TABLE_NAME,
arrayOf(PlaceContract.COLUMN_NAME_NAME,
PlaceContract.COLUMN_NAME_ADDRESS,
PlaceContract.COLUMN_NAME_TYPE),
"${PlaceContract.COLUMN_NAME_NAME} like ?",
arrayOf(searchResult),
null,
null,
"${PlaceContract.COLUMN_NAME_NAME} ASC"
)

while (cursor.moveToNext()) {
val name = cursor.getString(
cursor.getColumnIndexOrThrow(PlaceContract.COLUMN_NAME_NAME)
)
val address = cursor.getString(
cursor.getColumnIndexOrThrow(PlaceContract.COLUMN_NAME_ADDRESS)
)
val type = cursor.getString(
cursor.getColumnIndexOrThrow(PlaceContract.COLUMN_NAME_TYPE))
resultList.add(Place(name, address, type))
}
cursor.close()
_place.value = resultList
}
}
3 changes: 3 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/SearchWord.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package campus.tech.kakao.map

data class SearchWord(val name: String, val address: String, val type: String)
11 changes: 11 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/SearchWordContract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package campus.tech.kakao.map

object SearchWordContract {
const val DB_NAME = "search_word.db"
const val DB_VERSION = 1
const val TABLE_NAME = "SearchWord"
const val COLUMN_NAME_NAME = "name"
const val COLUMN_NAME_ADDRESS = "address"
const val COLUMN_NAME_TYPE = "type"

}
Loading