diff --git a/README.md b/README.md index df2a6477..62f52f72 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ # android-map-keyword + +## 1단계 +- [x] 기본 레이아웃 구현 +- [x] 로컬 데이터베이스 구현 +- [x] 검색 결과 표시 레이아웃 구현 + - [x] 결과 목록 레이아웃 + - [x] 결과 아이템 레이아웃 + +## 2단계 +- [x] 검색어를 입력하면 검색 결과 목록 표시 +- [x] 검색 결과 목록 스크롤 가능 +- [x] 입력한 검색어 X 버튼을 통해 삭제 가능 +- [x] 검색 결과에서 항목 선택 시 검색어 저장 목록에 추가 +- [x] 저장된 검색어 가로 스크롤 가능 +- [x] 저장된 검색어 X 버튼을 통해 삭제 가능 +- [x] 저장된 검색어 DB에 저장 \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..9f4da0b8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ tools:targetApi="31"> diff --git a/app/src/main/java/campus/tech/kakao/map/DatabaseListener.kt b/app/src/main/java/campus/tech/kakao/map/DatabaseListener.kt new file mode 100644 index 00000000..863926cc --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/DatabaseListener.kt @@ -0,0 +1,8 @@ +package campus.tech.kakao.map + +interface DatabaseListener { + fun deleteHistory(historyName: String) + fun insertHistory(historyName: String) + fun updateSearchResult() + fun updateSearchHistory() +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/HistoryRecyclerAdapter.kt b/app/src/main/java/campus/tech/kakao/map/HistoryRecyclerAdapter.kt new file mode 100644 index 00000000..a0b135a3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/HistoryRecyclerAdapter.kt @@ -0,0 +1,49 @@ +package campus.tech.kakao.map + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder + +class HistoryRecyclerAdapter( + var history: List, + val layoutInflater: LayoutInflater, + val databaseListener: DatabaseListener +) : RecyclerView.Adapter() { + inner class HistoryViewHolder(itemView: View) : ViewHolder(itemView) { + val name: TextView = itemView.findViewById(R.id.history_name) + val clear: ImageButton = itemView.findViewById(R.id.history_clear) + + init { + clear.setOnClickListener { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + databaseListener.deleteHistory(name.text.toString()) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder { + val view = layoutInflater.inflate(R.layout.item_search_history, parent, false) + return HistoryViewHolder(view) + } + + override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) { + holder.name.text = history[position] + } + + override fun getItemCount(): Int { + return history.size + } + + fun refreshList() { + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/Location.kt b/app/src/main/java/campus/tech/kakao/map/Location.kt new file mode 100644 index 00000000..0d893493 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Location.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map + +data class Location( + val name: String, + val category: String, + val address: String +) { + companion object { + const val CAFE: String = "카페" + const val PHARMACY: String = "약국" + const val RESTAURANT: String = "식당" + const val NORMAL: String = "일반" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt index 95b43803..460324b8 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -1,11 +1,104 @@ package campus.tech.kakao.map import android.os.Bundle +import android.widget.EditText +import android.widget.ImageButton +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class MainActivity : AppCompatActivity(), DatabaseListener { + private lateinit var viewModel: MapViewModel + private lateinit var searchBox: EditText + private lateinit var searchHistoryView: RecyclerView + private lateinit var searchResultView: RecyclerView + private lateinit var message: TextView + private lateinit var clear: ImageButton + + private lateinit var searchResultAdapter: ResultRecyclerAdapter + private lateinit var searchHistoryAdapter: HistoryRecyclerAdapter -class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + viewModel = MapViewModel(this, this) + searchBox = findViewById(R.id.search_box) + searchHistoryView = findViewById(R.id.search_history) + searchResultView = findViewById(R.id.search_result) + message = findViewById(R.id.message) + clear = findViewById(R.id.clear) + + searchBox.doAfterTextChanged { + it?.let { + search(it.toString(), false) + } + } + + clear.setOnClickListener { + searchBox.text.clear() + } + + initSearchResultView() + initSearchHistoryView() + observeData() + } + + override fun deleteHistory(historyName: String) { + viewModel.deleteHistory(historyName) + } + + override fun insertHistory(historyName: String) { + viewModel.insertHistory(historyName) + } + + override fun updateSearchResult() { + val searchResult = viewModel.searchResult.value!! + searchResultAdapter.refreshList() + + if (searchResult.isNotEmpty() && searchBox.text.isNotEmpty()) { + searchResultView.isVisible = true + message.isVisible = false + } else { + searchResultView.isVisible = false + message.isVisible = true + } + } + + override fun updateSearchHistory() { + searchHistoryAdapter.refreshList() + } + + private fun search(locName: String, isExactMatch: Boolean) { + viewModel.searchLocation(locName, isExactMatch) + } + + private fun initSearchResultView() { + searchResultAdapter = + ResultRecyclerAdapter(viewModel.searchResult.value!!, layoutInflater, this) + searchResultView.adapter = searchResultAdapter + searchResultView.layoutManager = + LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false) + } + + private fun initSearchHistoryView() { + searchHistoryAdapter = + HistoryRecyclerAdapter(viewModel.getAllHistory(), layoutInflater, this) + searchHistoryView.adapter = searchHistoryAdapter + searchHistoryView.layoutManager = + LinearLayoutManager(this@MainActivity, LinearLayoutManager.HORIZONTAL, false) + } + + private fun observeData() { + viewModel.searchHistory.observe(this, Observer { + searchHistoryAdapter.history = it + }) + viewModel.searchResult.observe(this, Observer { + searchResultAdapter.searchResult = it + }) } } diff --git a/app/src/main/java/campus/tech/kakao/map/MapContract.kt b/app/src/main/java/campus/tech/kakao/map/MapContract.kt new file mode 100644 index 00000000..dd2a18c2 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapContract.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map + +import android.provider.BaseColumns + +object MapContract { + object MapEntry : BaseColumns { + const val TABLE_NAME = "map" + const val TABLE_NAME_HISTORY = "history" + const val COLUMN_NAME_NAME = "name" + const val COLUMN_NAME_CATEGORY = "category" + const val COLUMN_NAME_ADDRESS = "address" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapDbHelper.kt b/app/src/main/java/campus/tech/kakao/map/MapDbHelper.kt new file mode 100644 index 00000000..71774fea --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapDbHelper.kt @@ -0,0 +1,61 @@ +package campus.tech.kakao.map + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.provider.BaseColumns + +class MapDbHelper(mContext: Context) : + SQLiteOpenHelper(mContext, DATABASE_NAME, null, DATABASE_VERSION) { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL(SQL_CREATE_ENTRIES) + db?.execSQL(SQL_CREATE_ENTRIES_HISTORY) + initializeDb(db) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL(SQL_DELETE_ENTRIES) + db?.execSQL(SQL_DELETE_ENTRIES_HISTORY) + onCreate(db) + } + + private fun initializeDb(db: SQLiteDatabase?) { + for (idx in 1..10) { + val exampleCafeValue = ContentValues() + exampleCafeValue.put(MapContract.MapEntry.COLUMN_NAME_NAME, "카페$idx") + exampleCafeValue.put(MapContract.MapEntry.COLUMN_NAME_CATEGORY, Location.CAFE) + exampleCafeValue.put(MapContract.MapEntry.COLUMN_NAME_ADDRESS, "서울 성동구 성수동 $idx") + db?.insert(MapContract.MapEntry.TABLE_NAME, null, exampleCafeValue) + + val examplePharValue = ContentValues() + examplePharValue.put(MapContract.MapEntry.COLUMN_NAME_NAME, "약국$idx") + examplePharValue.put(MapContract.MapEntry.COLUMN_NAME_CATEGORY, Location.PHARMACY) + examplePharValue.put(MapContract.MapEntry.COLUMN_NAME_ADDRESS, "서울 성동구 성수동 $idx") + db?.insert(MapContract.MapEntry.TABLE_NAME, null, examplePharValue) + } + } + + companion object { + const val DATABASE_NAME = "map.db" + const val DATABASE_VERSION = 1 + + private const val SQL_CREATE_ENTRIES = + "CREATE TABLE ${MapContract.MapEntry.TABLE_NAME} (" + + "${BaseColumns._ID} INTEGER PRIMARY KEY AUTOINCREMENT," + + "${MapContract.MapEntry.COLUMN_NAME_NAME} TEXT," + + "${MapContract.MapEntry.COLUMN_NAME_CATEGORY} TEXT," + + "${MapContract.MapEntry.COLUMN_NAME_ADDRESS} TEXT" + + ");" + private const val SQL_DELETE_ENTRIES = + "DROP TABLE IF EXISTS ${MapContract.MapEntry.TABLE_NAME}" + + private const val SQL_CREATE_ENTRIES_HISTORY = + "CREATE TABLE ${MapContract.MapEntry.TABLE_NAME_HISTORY} (" + + "${BaseColumns._ID} INTEGER PRIMARY KEY AUTOINCREMENT," + + "${MapContract.MapEntry.COLUMN_NAME_NAME} TEXT" + + ");" + private const val SQL_DELETE_ENTRIES_HISTORY = + "DROP TABLE IF EXISTS ${MapContract.MapEntry.TABLE_NAME_HISTORY}" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapModel.kt b/app/src/main/java/campus/tech/kakao/map/MapModel.kt new file mode 100644 index 00000000..e8d18b69 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapModel.kt @@ -0,0 +1,126 @@ +package campus.tech.kakao.map + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor + +class MapModel(mContext: Context) { + private val helper: MapDbHelper = MapDbHelper(mContext) + + fun insertLocation(location: Location) { + val writableDb = helper.writableDatabase + val content = ContentValues() + content.put(MapContract.MapEntry.COLUMN_NAME_NAME, location.name) + content.put(MapContract.MapEntry.COLUMN_NAME_CATEGORY, location.category) + content.put(MapContract.MapEntry.COLUMN_NAME_ADDRESS, location.address) + + writableDb.insert(MapContract.MapEntry.TABLE_NAME, null, content) + } + + fun getSearchedLocation(locName: String, isExactMatch: Boolean): List { + val readableDb = helper.readableDatabase + + val selection = "${MapContract.MapEntry.COLUMN_NAME_NAME} LIKE ?" + val selectionArgs = arrayOf("%${locName}%") + val cursor = readableDb.query( + MapContract.MapEntry.TABLE_NAME, + null, + selection, + selectionArgs, + null, + null, + null + ) + return getLocationResult(cursor) + } + + fun getAllLocation(): List { + val readableDb = helper.readableDatabase + val cursor = readableDb.query( + MapContract.MapEntry.TABLE_NAME, + null, + null, + null, + null, + null, + null + ) + return getLocationResult(cursor) + } + + private fun getLocationResult(cursor: Cursor): List { + val res = mutableListOf() + while (cursor.moveToNext()) { + res.add(getLocation(cursor)) + } + cursor.close() + return res + } + + private fun getLocation(cursor: Cursor): Location { + val name = + cursor.getString(cursor.getColumnIndexOrThrow(MapContract.MapEntry.COLUMN_NAME_NAME)) + val category = + cursor.getString(cursor.getColumnIndexOrThrow(MapContract.MapEntry.COLUMN_NAME_CATEGORY)) + val address = + cursor.getString(cursor.getColumnIndexOrThrow(MapContract.MapEntry.COLUMN_NAME_ADDRESS)) + + return Location(name, category, address) + } + + fun insertHistory(locName: String) { + + if (isHistoryExist(locName)) + deleteHistory(locName) + val writeableDb = helper.writableDatabase + val content = ContentValues() + content.put(MapContract.MapEntry.COLUMN_NAME_NAME, locName) + writeableDb.insert(MapContract.MapEntry.TABLE_NAME_HISTORY, null, content) + } + + private fun isHistoryExist(locName: String): Boolean { + val readableDb = helper.readableDatabase + val selection = "${MapContract.MapEntry.COLUMN_NAME_NAME} = ?" + val selectionArgs = arrayOf(locName) + val cursor = readableDb.query( + MapContract.MapEntry.TABLE_NAME_HISTORY, + null, + selection, + selectionArgs, + null, + null, + null + ) + val isExist: Boolean = cursor.moveToNext() + cursor.close() + return isExist + } + + fun deleteHistory(locName: String) { + val writeableDb = helper.writableDatabase + val selection = "${MapContract.MapEntry.COLUMN_NAME_NAME} = ?" + val selectionArgs = arrayOf(locName) + + writeableDb.delete(MapContract.MapEntry.TABLE_NAME_HISTORY, selection, selectionArgs) + } + + fun getAllHistory(): List { + val readableDb = helper.readableDatabase + val cursor = readableDb.query( + MapContract.MapEntry.TABLE_NAME_HISTORY, + null, + null, + null, + null, + null, + null + ) + + val res = mutableListOf() + while (cursor.moveToNext()) { + res.add(cursor.getString(cursor.getColumnIndexOrThrow(MapContract.MapEntry.COLUMN_NAME_NAME))) + } + cursor.close() + return res + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/MapViewModel.kt new file mode 100644 index 00000000..57cf1b26 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapViewModel.kt @@ -0,0 +1,50 @@ +package campus.tech.kakao.map + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class MapViewModel(mContext: Context, val databaseListener: DatabaseListener) : ViewModel() { + private val model = MapModel(mContext) + private val _searchResult: MutableLiveData> + val searchResult: LiveData> + private val _searchHistory: MutableLiveData> + val searchHistory: LiveData> + + init { + _searchResult = MutableLiveData(model.getSearchedLocation("", false)) + searchResult = _searchResult + _searchHistory = MutableLiveData(model.getAllHistory()) + searchHistory = _searchHistory + } + + fun insertLocation(location: Location) { + model.insertLocation(location) + } + + fun searchLocation(locName: String, isExactMatch: Boolean) { + _searchResult.value = model.getSearchedLocation(locName, isExactMatch) + databaseListener.updateSearchResult() + } + + fun getAllLocation(): List { + return model.getAllLocation() + } + + fun deleteHistory(historyName: String) { + model.deleteHistory(historyName) + _searchHistory.value = model.getAllHistory() + databaseListener.updateSearchHistory() + } + + fun insertHistory(historyName: String) { + model.insertHistory(historyName) + _searchHistory.value = model.getAllHistory() + databaseListener.updateSearchHistory() + } + + fun getAllHistory(): List { + return model.getAllHistory() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ResultRecyclerAdapter.kt b/app/src/main/java/campus/tech/kakao/map/ResultRecyclerAdapter.kt new file mode 100644 index 00000000..d09abe99 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ResultRecyclerAdapter.kt @@ -0,0 +1,48 @@ +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.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder + +class ResultRecyclerAdapter( + var searchResult: List, + val layoutInflater: LayoutInflater, + val databaseListener: DatabaseListener +) : RecyclerView.Adapter() { + inner class MapViewHolder(itemView: View) : ViewHolder(itemView) { + val name: TextView = itemView.findViewById(R.id.location_name) + val category: TextView = itemView.findViewById(R.id.location_category) + val address: TextView = itemView.findViewById(R.id.location_address) + + init { + itemView.setOnClickListener { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + databaseListener.insertHistory(name.text.toString()) + databaseListener.updateSearchHistory() + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MapViewHolder { + val view = layoutInflater.inflate(R.layout.item_search_result, parent, false) + return MapViewHolder(view) + } + + override fun getItemCount(): Int { + return searchResult.size + } + + override fun onBindViewHolder(holder: MapViewHolder, position: Int) { + holder.name.text = searchResult[position].name + holder.category.text = searchResult[position].category + holder.address.text = searchResult[position].address + } + + fun refreshList() { + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/background_item_search_history.xml b/app/src/main/res/drawable/background_item_search_history.xml new file mode 100644 index 00000000..d927da07 --- /dev/null +++ b/app/src/main/res/drawable/background_item_search_history.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/clear_24.xml b/app/src/main/res/drawable/clear_24.xml new file mode 100644 index 00000000..8f89359b --- /dev/null +++ b/app/src/main/res/drawable/clear_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/location_24.xml b/app/src/main/res/drawable/location_24.xml new file mode 100644 index 00000000..d7f69831 --- /dev/null +++ b/app/src/main/res/drawable/location_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..1c8be102 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,50 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + + app:layout_constraintTop_toBottomOf="@id/search_box" /> + + + diff --git a/app/src/main/res/layout/item_search_history.xml b/app/src/main/res/layout/item_search_history.xml new file mode 100644 index 00000000..a5c6c465 --- /dev/null +++ b/app/src/main/res/layout/item_search_history.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml new file mode 100644 index 00000000..07f57fd4 --- /dev/null +++ b/app/src/main/res/layout/item_search_result.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 768b058a..0543b4fc 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,5 @@ #FF000000 #FFFFFFFF + #998BC34A diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..0c8b69f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ Map + 검색 결과가 없습니다. + 검색어를 입력해 주세요. + 삭제 \ No newline at end of file