diff --git a/README.md b/README.md index df2a6477..7515fdd1 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# android-map-keyword +# android-map-keyword STEP 2 + +## 개요 +android-map-keyword STEP2에서는 STEP1에서 생성한 로컬 데이터베이스에 들어있는 장소들에 대한 검색 기능을 구현합니다. + +## 기능 +- 검색어를 입력하면 검색 결과 세로 스크롤이 되는 검색 결과 목록이 표시됨 +- 검색어는 X를 눌러서 삭제 가능 +- 검색 결과 목록에서 하나의 항목을 선택 가능, 선택된 항목은 가로 스크롤 가능한 검색어 저장 목록에 추가 +- 저장된 검색어는 X를 눌러서 삭제 가능, 앱을 재실행해도 검색어 저장 목록은 유지 \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..fc4e64e2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:theme="@style/Theme.Map" tools:targetApi="31"> diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt deleted file mode 100644 index 95b43803..00000000 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package campus.tech.kakao.map - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/Place.kt b/app/src/main/java/campus/tech/kakao/map/Place.kt new file mode 100644 index 00000000..8ef87707 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Place.kt @@ -0,0 +1,8 @@ +package campus.tech.kakao.map + +data class Place( + val idx: Int, + val name: String, + val category: String, + val address: String +) diff --git a/app/src/main/java/campus/tech/kakao/map/PlaceDBHelper.kt b/app/src/main/java/campus/tech/kakao/map/PlaceDBHelper.kt new file mode 100644 index 00000000..8163d9ee --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/PlaceDBHelper.kt @@ -0,0 +1,72 @@ +package campus.tech.kakao.map + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +class PlaceDBHelper(context: Context) : SQLiteOpenHelper(context, "Place.db", null, 2) { + + override fun onCreate(db: SQLiteDatabase?) { + createPlaceTable(db) + insertInitialPlaceData(db) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL("DROP TABLE IF EXISTS PlaceTable") + onCreate(db) + } + + private fun createPlaceTable(db: SQLiteDatabase?) { + val placeTableSQL = """ + CREATE TABLE IF NOT EXISTS PlaceTable ( + idx INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + category TEXT NOT NULL, + address TEXT NOT NULL + ) + """ + db?.execSQL(placeTableSQL) + } + + private val context: Context = context.applicationContext + private fun insertInitialPlaceData(db: SQLiteDatabase?) { + val initialPlaces = listOf( + Place(1, context.getString(R.string.place_kakao_cafe), context.getString(R.string.category_cafe), context.getString(R.string.place_address_1)), + Place(2, context.getString(R.string.place_kakao_restaurant), context.getString(R.string.category_restaurant), context.getString(R.string.place_address_2)), + Place(3, context.getString(R.string.place_kakao_pub), context.getString(R.string.category_pub), context.getString(R.string.place_address_3)), + Place(4, context.getString(R.string.place_tech_cafe), context.getString(R.string.category_cafe), context.getString(R.string.place_address_4)), + Place(5, context.getString(R.string.place_tech_restaurant), context.getString(R.string.category_restaurant), context.getString(R.string.place_address_5)), + Place(6, context.getString(R.string.place_tech_pub), context.getString(R.string.category_pub), context.getString(R.string.place_address_6)), + Place(7, context.getString(R.string.place_campus_cafe), context.getString(R.string.category_cafe), context.getString(R.string.place_address_7)), + Place(8, context.getString(R.string.place_campus_restaurant), context.getString(R.string.category_restaurant), context.getString(R.string.place_address_8)), + Place(9, context.getString(R.string.place_campus_pub), context.getString(R.string.category_pub), context.getString(R.string.place_address_9)) + ) + + initialPlaces.forEach { place -> + val values = ContentValues().apply { + put("name", place.name) + put("category", place.category) + put("address", place.address) + } + db?.insert("PlaceTable", null, values) + } + } + + fun getAllPlaces(): MutableList { + val db = readableDatabase + val cursor: Cursor = db.rawQuery("SELECT * FROM PlaceTable", null) + val places = mutableListOf() + if (cursor.moveToFirst()) { + do { + val idx = cursor.getInt(cursor.getColumnIndexOrThrow("idx")) + val name = cursor.getString(cursor.getColumnIndexOrThrow("name")) + val category = cursor.getString(cursor.getColumnIndexOrThrow("category")) + val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) + places.add(Place(idx, name, category, address)) + } while (cursor.moveToNext()) + } + cursor.close() + return places + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt new file mode 100644 index 00000000..6450f652 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt @@ -0,0 +1,103 @@ +package campus.tech.kakao.map + +import android.os.Bundle +import android.util.Log +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class SearchActivity : AppCompatActivity() { + + private lateinit var searchView: SearchView + private lateinit var resultRecyclerView: RecyclerView + private lateinit var searchHistoryRecyclerView: RecyclerView + private lateinit var noResults: TextView + private lateinit var resultRecyclerViewAdapter: ResultRecyclerViewAdapter + private lateinit var searchHistoryRecyclerViewAdapter: SearchHistoryRecyclerViewAdapter + private lateinit var placeList: List + private lateinit var searchHistoryList: MutableList + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_search) + searchView = findViewById(R.id.search_view) + resultRecyclerView = findViewById(R.id.recycler_view) + searchHistoryRecyclerView = findViewById(R.id.horizontal_recycler_view) + noResults = findViewById(R.id.no_results) + + val placeDB = PlaceDBHelper(this) + val searchHistoryDB = SearchHistoryDBHelper(this) + + placeList = placeDB.getAllPlaces() + searchHistoryList = searchHistoryDB.getAllSearchHistory() + resultRecyclerViewAdapter = ResultRecyclerViewAdapter( + places = emptyList(), + onItemClick = { place -> + searchHistoryDB.insertSearchHistory(place) + updateSearchHistoryRecyclerView(place) + } + ) + resultRecyclerView.adapter = resultRecyclerViewAdapter + resultRecyclerView.layoutManager = LinearLayoutManager(this) + searchHistoryRecyclerViewAdapter = SearchHistoryRecyclerViewAdapter( + searchHistory = searchHistoryList, + onItemClick = { index -> + searchView.setQuery(searchHistoryList[index].name, true) + searchView.clearFocus() + + searchView.isIconified = false + }, + onItemDelete = { index -> + if (index >= 0 && index < searchHistoryList.size) { + val deletedItemName = searchHistoryList[index].name + searchHistoryList.removeAt(index) + searchHistoryDB.deleteSearchHistoryByName(deletedItemName) + searchHistoryRecyclerViewAdapter.notifyItemRemoved(index) + } + } + ) + searchHistoryRecyclerView.adapter = searchHistoryRecyclerViewAdapter + searchHistoryRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + filterPlaces(newText) + return true + } + }) + } + + private fun filterPlaces(query: String?) { + val filteredList = if (query.isNullOrBlank()) emptyList() else placeList.filter { place -> matchesQuery(place, query) } + resultRecyclerViewAdapter.setPlaces(filteredList) + showNoResultsMessage(filteredList.isEmpty()) + } + + private fun matchesQuery(place: Place, searchQuery: String): Boolean { + val queryLowercase = searchQuery.lowercase() + return place.name.lowercase().contains(queryLowercase) || + place.category.lowercase().contains(queryLowercase) || + place.address.lowercase().contains(queryLowercase) + } + + + private fun showNoResultsMessage(show: Boolean) { + if (show) { + noResults.visibility = TextView.VISIBLE + resultRecyclerView.visibility = RecyclerView.GONE + } else { + noResults.visibility = TextView.GONE + resultRecyclerView.visibility = RecyclerView.VISIBLE + } + } + + private fun updateSearchHistoryRecyclerView(place: Place) { + searchHistoryList.add(place) + searchHistoryRecyclerViewAdapter.notifyDataSetChanged() + } +} + diff --git a/app/src/main/java/campus/tech/kakao/map/SearchHistoryDBHelper.kt b/app/src/main/java/campus/tech/kakao/map/SearchHistoryDBHelper.kt new file mode 100644 index 00000000..1f3c081d --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchHistoryDBHelper.kt @@ -0,0 +1,63 @@ +package campus.tech.kakao.map + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +class SearchHistoryDBHelper(context: Context) : SQLiteOpenHelper(context, "SearchHistory.db", null, 1) { + + override fun onCreate(db: SQLiteDatabase?) { + createSearchHistoryTable(db) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL("DROP TABLE IF EXISTS SearchHistoryTable") + onCreate(db) + } + + private fun createSearchHistoryTable(db: SQLiteDatabase?) { + val searchHistoryTableSQL = """ + CREATE TABLE SearchHistoryTable ( + idx INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + category TEXT NOT NULL, + address TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """ + db?.execSQL(searchHistoryTableSQL) + } + + fun insertSearchHistory(place: Place): Long { + val db = writableDatabase + val values = ContentValues().apply { + put("name", place.name) + put("category", place.category) + put("address", place.address) + } + return db.insert("SearchHistoryTable", null, values) + } + + fun deleteSearchHistoryByName(name: String): Int { + val db = writableDatabase + return db.delete("SearchHistoryTable", "`name` = ?", arrayOf(name)) + } + + fun getAllSearchHistory(): MutableList { + val db = readableDatabase + val cursor: Cursor = db.rawQuery("SELECT * FROM SearchHistoryTable ORDER BY timestamp DESC", null) + val searchHistory = mutableListOf() + if (cursor.moveToFirst()) { + do { + val idx = cursor.getInt(cursor.getColumnIndexOrThrow("idx")) + val name = cursor.getString(cursor.getColumnIndexOrThrow("name")) + val category = cursor.getString(cursor.getColumnIndexOrThrow("category")) + val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) + searchHistory.add(Place(idx, name, category, address)) + } while (cursor.moveToNext()) + } + cursor.close() + return searchHistory + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/SearchHistoryRecyclerViewAdapter.kt b/app/src/main/java/campus/tech/kakao/map/SearchHistoryRecyclerViewAdapter.kt new file mode 100644 index 00000000..70da993c --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchHistoryRecyclerViewAdapter.kt @@ -0,0 +1,33 @@ +package campus.tech.kakao.map + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView + +class SearchHistoryRecyclerViewAdapter( + private var searchHistory: MutableList, + private val onItemClick: (Int) -> Unit, + private val onItemDelete: (Int) -> Unit +) : RecyclerView.Adapter() { + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val searchHistoryBtn: TextView = itemView.findViewById(R.id.search_history_item) + val searchHistoryDeleteBtn: ImageView = itemView.findViewById(R.id.search_history_delete_button) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_search_history, parent, false) + return ViewHolder(view) + } + override fun onBindViewHolder(holder: ViewHolder, index: Int) { + holder.searchHistoryBtn.text = searchHistory[index].name + holder.searchHistoryBtn.setOnClickListener { onItemClick(index) } + holder.searchHistoryDeleteBtn.setOnClickListener { + onItemDelete(index) + notifyItemRemoved(index) + notifyItemRangeChanged(index, searchHistory.size); + } + } + override fun getItemCount(): Int { return searchHistory.size } +} diff --git a/app/src/main/java/campus/tech/kakao/map/SearchResultRecyclerViewAdapter.kt b/app/src/main/java/campus/tech/kakao/map/SearchResultRecyclerViewAdapter.kt new file mode 100644 index 00000000..cc1a57cc --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchResultRecyclerViewAdapter.kt @@ -0,0 +1,41 @@ +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 + +class ResultRecyclerViewAdapter( + private var places: List, + private val onItemClick: (Place) -> Unit +) : RecyclerView.Adapter() { + + class PlaceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val placeName: TextView = itemView.findViewById(R.id.place_name) + val placeCategory: TextView = itemView.findViewById(R.id.place_category) + val placeAddress: TextView = itemView.findViewById(R.id.place_address) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_search_result, parent, false) + return PlaceViewHolder(view) + } + + override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) { + val place = places[position] + holder.placeName.text = place.name + holder.placeCategory.text = place.category + holder.placeAddress.text = place.address + holder.itemView.setOnClickListener { onItemClick(place) } + } + + override fun getItemCount(): Int { + return places.size + } + + fun setPlaces(newPlaces: List) { + places = newPlaces + notifyDataSetChanged() + } +} diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 00000000..e0278034 --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/item_search_history_background.xml b/app/src/main/res/drawable/item_search_history_background.xml new file mode 100644 index 00000000..fdf40547 --- /dev/null +++ b/app/src/main/res/drawable/item_search_history_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/location.xml b/app/src/main/res/drawable/location.xml new file mode 100644 index 00000000..9e71ddca --- /dev/null +++ b/app/src/main/res/drawable/location.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/search_bar_background.xml b/app/src/main/res/drawable/search_bar_background.xml new file mode 100644 index 00000000..8efcfb53 --- /dev/null +++ b/app/src/main/res/drawable/search_bar_background.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 24d17df2..00000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml new file mode 100644 index 00000000..4690ecad --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + 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..4e372778 --- /dev/null +++ b/app/src/main/res/layout/item_search_history.xml @@ -0,0 +1,28 @@ + + + + + + + + \ 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..9f510caf --- /dev/null +++ b/app/src/main/res/layout/item_search_result.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..b71c1d37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,30 @@ Map + 검색어를 입력해 주세요. + 검색 결과가 없습니다. + + 카카오 카페 + 카카오 식당 + 카카오 포차 + 테크 카페 + 테크 식당 + 테크 포차 + 캠퍼스 카페 + 캠퍼스 식당 + 캠퍼스 포차 + + 카페 + 식당 + 주점 + + 금정구 1 + 금정구 2 + 금정구 3 + 금정구 4 + 금정구 5 + 금정구 6 + 금정구 7 + 금정구 8 + 금정구 9 + \ No newline at end of file