From 693dc6483039c0001178093654fd200d4c718617 Mon Sep 17 00:00:00 2001 From: changukLee Date: Mon, 1 Jul 2024 14:40:12 +0900 Subject: [PATCH 01/13] docs: update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - repository 설명 추가 - 1단계 기능 리스트 추가 --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index df2a6477..2b97bbdb 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ # android-map-keyword + +- 카카오 테크 캠퍼스 과제(검색어 저장) 수행을 위한 저장소입니다. + +## feature + +### 1단계 - 로컬 데이터 + +1. 검색어 입력 및 검색 결과를 표시할 기본 레이아웃 구현 + +2. 검색에 사용될 데이터를 로컬 베이스에 생성 + - SQLite를 사용 \ No newline at end of file From 4fcc38675ccaefddc8c64ac40fff54ab3eef1524 Mon Sep 17 00:00:00 2001 From: changukLee Date: Mon, 1 Jul 2024 14:44:58 +0900 Subject: [PATCH 02/13] design: add Base Search Layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검색어 입력 및 검색 결과를 표시할 기본 레이아웃 구현 --- app/src/main/res/drawable/baseline_clear.xml | 5 +++ app/src/main/res/layout/activity_main.xml | 36 ++++++++++++++++++-- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_clear.xml diff --git a/app/src/main/res/drawable/baseline_clear.xml b/app/src/main/res/drawable/baseline_clear.xml new file mode 100644 index 00000000..f8ca0c64 --- /dev/null +++ b/app/src/main/res/drawable/baseline_clear.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..1a51592a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,15 +5,47 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_marginStart="15dp" + android:layout_marginEnd="15dp" tools:context=".MainActivity"> + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/search_edit_layout" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..084ce9f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ Map + 검색 결과가 없습니다. + 검색어를 입력해 주세요. \ No newline at end of file From e93e703126dae82e06fc04da89373338f087a8e4 Mon Sep 17 00:00:00 2001 From: changukLee Date: Mon, 1 Jul 2024 22:38:34 +0900 Subject: [PATCH 03/13] feat: update Insert Data Feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLite를 이용해 로컬 DB에 데이터를 저장하는 기능 구현 --- .../campus/tech/kakao/map/MainActivity.kt | 15 ++++++ .../kakao/map/data/PlaceDatabaseHelper.kt | 49 +++++++++++++++++++ .../tech/kakao/map/data/PlaceRepository.kt | 16 ++++++ .../java/campus/tech/kakao/map/model/Place.kt | 7 +++ 4 files changed, 87 insertions(+) create mode 100644 app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/Place.kt 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..a12261a9 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -2,10 +2,25 @@ package campus.tech.kakao.map import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.data.PlaceRepository +import campus.tech.kakao.map.model.Place class MainActivity : AppCompatActivity() { + private lateinit var placeRepository: PlaceRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + placeRepository = PlaceRepository(this) + placeRepository.clearAllPlaces() // 테스트용 + + insertPlaces(placeRepository, "카페") + insertPlaces(placeRepository, "식당") + } + + private fun insertPlaces(placeRepository: PlaceRepository, category:String) { + repeat(20) { idx -> + placeRepository.insertPlace(Place(category + (idx + 1), "서울 성동구 성수동 ${idx + 1}", category)) + } } } diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt new file mode 100644 index 00000000..95b11bd7 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt @@ -0,0 +1,49 @@ +package campus.tech.kakao.map.data + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import campus.tech.kakao.map.model.Place + +class PlaceDatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + + @Override + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE $TABLE_NAME (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, category TEXT)") + } + + @Override + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } + + /** + * db에 place를 insert하는 함수. + */ + fun insertPlace(place: Place) { + val db = writableDatabase + val contentValues = ContentValues().apply { + put("name", place.name) + put("address", place.address) + put("category", place.category) + } + db.insert(TABLE_NAME, null, contentValues) + } + + /** + * 테이블을 삭제하는 함수. + */ + fun clearAllPlaces() { + val db = writableDatabase + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } + + companion object { + const val DATABASE_NAME = "place.db" + const val DATABASE_VERSION = 1 + const val TABLE_NAME = "place_data" + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt new file mode 100644 index 00000000..db66930f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map.data + +import android.content.Context +import campus.tech.kakao.map.model.Place + +class PlaceRepository(context: Context) { + private val dbHelper = PlaceDatabaseHelper(context) + + fun insertPlace(place: Place) { + dbHelper.insertPlace(place) + } + + fun clearAllPlaces() { + dbHelper.clearAllPlaces() + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/model/Place.kt b/app/src/main/java/campus/tech/kakao/map/model/Place.kt new file mode 100644 index 00000000..b1598b03 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/Place.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.model + +data class Place( + val name: String, + val address: String? = null, + val category: String? = null +) From 06918798ac9dad23bec101aea320c17c9d73cba0 Mon Sep 17 00:00:00 2001 From: changukLee Date: Wed, 3 Jul 2024 22:24:27 +0900 Subject: [PATCH 04/13] docs: update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2단계 기능 리스트 추가 --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b97bbdb..ae76538b 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,17 @@ 1. 검색어 입력 및 검색 결과를 표시할 기본 레이아웃 구현 2. 검색에 사용될 데이터를 로컬 베이스에 생성 - - SQLite를 사용 \ No newline at end of file + - SQLite를 사용 + +### 2단계 - 검색 + +1. 검색어를 입력하면 검색 결과 목록이 표시된다. + - 리사이클러뷰 사용(세로 스크롤) + +2. 입력한 검색어는 X를 누럴서 삭제할 수 있다. + +3. 검색 결과 목록에서 하나의 항목을 선택할 수 있다. + - 선택된 항목은 검색어 저장 목록에 추가된다. + - 리사이클러뷰 사용(가로 스크롤) + +4. 저장된 검색어는 X를 눌러서 삭제할 수 있다. \ No newline at end of file From 2ca8020b94fa5c659fc2b9ac9c3f645d104bac57 Mon Sep 17 00:00:00 2001 From: changukLee Date: Wed, 3 Jul 2024 22:28:14 +0900 Subject: [PATCH 05/13] feat: merge PlaceDatabaseHelper into PlaceRepository --- .../kakao/map/data/PlaceDatabaseHelper.kt | 49 ----------------- .../tech/kakao/map/data/PlaceRepository.kt | 55 +++++++++++++++++-- 2 files changed, 51 insertions(+), 53 deletions(-) delete mode 100644 app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt deleted file mode 100644 index 95b11bd7..00000000 --- a/app/src/main/java/campus/tech/kakao/map/data/PlaceDatabaseHelper.kt +++ /dev/null @@ -1,49 +0,0 @@ -package campus.tech.kakao.map.data - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import campus.tech.kakao.map.model.Place - -class PlaceDatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { - - @Override - override fun onCreate(db: SQLiteDatabase) { - db.execSQL("CREATE TABLE $TABLE_NAME (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, category TEXT)") - } - - @Override - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") - onCreate(db) - } - - /** - * db에 place를 insert하는 함수. - */ - fun insertPlace(place: Place) { - val db = writableDatabase - val contentValues = ContentValues().apply { - put("name", place.name) - put("address", place.address) - put("category", place.category) - } - db.insert(TABLE_NAME, null, contentValues) - } - - /** - * 테이블을 삭제하는 함수. - */ - fun clearAllPlaces() { - val db = writableDatabase - db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") - onCreate(db) - } - - companion object { - const val DATABASE_NAME = "place.db" - const val DATABASE_VERSION = 1 - const val TABLE_NAME = "place_data" - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt index db66930f..d5841d54 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt @@ -1,16 +1,63 @@ package campus.tech.kakao.map.data +import android.content.ContentValues import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper import campus.tech.kakao.map.model.Place -class PlaceRepository(context: Context) { - private val dbHelper = PlaceDatabaseHelper(context) +class PlaceRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE $TABLE_NAME (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, category TEXT)") + } + + override fun onUpgrade( + db: SQLiteDatabase, + oldVersion: Int, + newVersion: Int, + ) { + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } fun insertPlace(place: Place) { - dbHelper.insertPlace(place) + val db = writableDatabase + val contentValues = + ContentValues().apply { + put(COLUMN_NAME, place.name) + put(COLUMN_ADDRESS, place.address) + put(COLUMN_CATEGORY, place.category) + } + db.insert(TABLE_NAME, null, contentValues) } fun clearAllPlaces() { - dbHelper.clearAllPlaces() + val db = writableDatabase + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } + + fun getPlacesByCategory(category: String): List { + val db = readableDatabase + val cursor = db.query(TABLE_NAME, null, "category=?", arrayOf(category), null, null, null) + val places = mutableListOf() + while (cursor.moveToNext()) { + val name = cursor.getString(cursor.getColumnIndexOrThrow("name")) + val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) + places.add(Place(name, address, category)) + } + cursor.close() + return places + } + + companion object { + const val DATABASE_NAME = "place.db" + const val DATABASE_VERSION = 1 + const val TABLE_NAME = "place_data" + + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_ADDRESS = "address" + const val COLUMN_CATEGORY = "category" } } From c70a41f9b4777adc9f0bbff744351adac1d39110 Mon Sep 17 00:00:00 2001 From: changukLee Date: Wed, 3 Jul 2024 22:31:39 +0900 Subject: [PATCH 06/13] feat: update Search Feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검색어를 입력하면 검색 결과 목록이 표시되도록 함 - MVVM 패턴과 data binding을 사용함 --- app/build.gradle.kts | 8 + .../campus/tech/kakao/map/MainActivity.kt | 148 ++++++++++++++++-- .../java/campus/tech/kakao/map/model/Place.kt | 2 +- .../kakao/map/viewmodel/PlaceViewModel.kt | 44 ++++++ .../res/drawable/baseline_location_pin.xml | 5 + app/src/main/res/layout/activity_main.xml | 106 ++++++++----- app/src/main/res/layout/item_place.xml | 72 +++++++++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 3 + 9 files changed, 342 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt create mode 100644 app/src/main/res/drawable/baseline_location_pin.xml create mode 100644 app/src/main/res/layout/item_place.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9932d6bb..3526d6b5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,8 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("org.jlleitschuh.gradle.ktlint") } android { @@ -36,6 +38,11 @@ android { buildFeatures { viewBinding = true + dataBinding = true + } + + dataBinding { + enable = true } } @@ -47,6 +54,7 @@ dependencies { implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.datastore:datastore-preferences:1.0.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 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 a12261a9..4aacb7a8 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -1,26 +1,156 @@ package campus.tech.kakao.map +import android.annotation.SuppressLint import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import campus.tech.kakao.map.data.PlaceRepository +import androidx.core.widget.addTextChangedListener +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.databinding.ActivityMainBinding +import campus.tech.kakao.map.databinding.ItemPlaceBinding import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.viewmodel.PlaceViewModel class MainActivity : AppCompatActivity() { - private lateinit var placeRepository: PlaceRepository + private lateinit var binding: ActivityMainBinding + private lateinit var placeViewModel: PlaceViewModel + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + placeViewModel = ViewModelProvider(this)[PlaceViewModel::class.java] + binding.viewModel = placeViewModel + binding.lifecycleOwner = this + + setSearchResultRecyclerView() + placeViewModel.clearAllPlaces() + testDataInsert() - placeRepository = PlaceRepository(this) - placeRepository.clearAllPlaces() // 테스트용 + setClearImageViewClickListener() + setSearchEditText() + observeSearchResults() + } - insertPlaces(placeRepository, "카페") - insertPlaces(placeRepository, "식당") + /** + * 테스트용 더미 데이터(카페, 약국)를 삽입하는 함수 + */ + private fun testDataInsert() { + insertPlaces("서울 성동구 성수동", "카페") + insertPlaces("서울 강남구 대치동", "약국") } - private fun insertPlaces(placeRepository: PlaceRepository, category:String) { + /** + * place 데이터를 db에 삽입하는 함수. + * + * @param address 저장할 주소값 + * @param category 저장할 카테고리값 + */ + private fun insertPlaces( + address: String, + category: String, + ) { repeat(20) { idx -> - placeRepository.insertPlace(Place(category + (idx + 1), "서울 성동구 성수동 ${idx + 1}", category)) + placeViewModel.insertPlace( + Place( + category + (idx + 1), + "$address ${idx + 1}", + category, + ), + ) + } + } + + /** + * 검색 결과를 표시하는 RecyclerView를 설정하는 함수. + */ + private fun setSearchResultRecyclerView() { + binding.searchResultRecyclerView.adapter = ResultRecyclerViewAdapter() + binding.searchResultRecyclerView.layoutManager = LinearLayoutManager(this) + } + + class ResultRecyclerViewAdapter : RecyclerView.Adapter() { + private var searchResultList: List = emptyList() + + /** + * RecyclerView에 표시할 장소 리스트를 설정하는 함수. + * + * @param places 장소 리스트 + */ + @SuppressLint("NotifyDataSetChanged") + fun setPlaces(places: List) { + searchResultList = places + notifyDataSetChanged() + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): PlaceViewHolder { + val binding = + ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PlaceViewHolder(binding) + } + + override fun getItemCount(): Int { + return searchResultList.size + } + + override fun onBindViewHolder( + holder: PlaceViewHolder, + position: Int, + ) { + holder.bind(searchResultList[position]) + } + + class PlaceViewHolder(private val binding: ItemPlaceBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(place: Place) { + binding.place = place + } + } + } + + /** + * 검색 EditText가 변경되면 placeViewModel을 통해 검색을 수행하도록 하는 함수. + */ + private fun setSearchEditText() { + binding.searchEditText.addTextChangedListener { editable -> + val searchText = editable.toString().trim() + if (searchText.isNotEmpty()) { + placeViewModel.searchPlaces(searchText) + } else { + placeViewModel.searchPlaces("") + } + } + } + + /** + * 검색 결과를 관찰하고, RecyclerView에 결과를 반영하는 함수. + */ + private fun observeSearchResults() { + placeViewModel.searchResults.observe( + this, + ) { places -> + if (places.isNotEmpty()) { + (binding.searchResultRecyclerView.adapter as ResultRecyclerViewAdapter).setPlaces( + places + ) + } + } + } + + /** + * clearImageView의 클릭 리스너를 설정하는 함수. + * + * searchEditText의 text를 null로 변경. + */ + private fun setClearImageViewClickListener() { + binding.clearImageView.setOnClickListener { + binding.searchEditText.text = null } } } diff --git a/app/src/main/java/campus/tech/kakao/map/model/Place.kt b/app/src/main/java/campus/tech/kakao/map/model/Place.kt index b1598b03..de84f7e0 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/Place.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/Place.kt @@ -3,5 +3,5 @@ package campus.tech.kakao.map.model data class Place( val name: String, val address: String? = null, - val category: String? = null + val category: String? = null, ) diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt new file mode 100644 index 00000000..32d1e9fa --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt @@ -0,0 +1,44 @@ +package campus.tech.kakao.map.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.data.PlaceRepository +import campus.tech.kakao.map.model.Place +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PlaceViewModel(application: Application) : AndroidViewModel(application) { + private val placeRepository: PlaceRepository = PlaceRepository(application) + private val _searchResults = MutableLiveData>() + + val searchResults: LiveData> get() = _searchResults + + fun insertPlace(place: Place) { + placeRepository.insertPlace(place) + } + + fun clearAllPlaces() { + placeRepository.clearAllPlaces() + } + + private fun clearSearchResults() { + _searchResults.value = emptyList() + } + + fun searchPlaces(category: String) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val results = placeRepository.getPlacesByCategory(category) + _searchResults.postValue(results) + } + } + } + + init { + clearSearchResults() + } +} diff --git a/app/src/main/res/drawable/baseline_location_pin.xml b/app/src/main/res/drawable/baseline_location_pin.xml new file mode 100644 index 00000000..47596f38 --- /dev/null +++ b/app/src/main/res/drawable/baseline_location_pin.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 1a51592a..f07d57a3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,51 +1,81 @@ - + xmlns:tools="http://schemas.android.com/tools"> + + + + + + + + android:layout_height="match_parent" + android:layout_marginStart="15dp" + android:layout_marginEnd="15dp" + tools:context=".MainActivity"> + + + + - + + + + - - + + + + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/search_edit_layout" /> - - - + + diff --git a/app/src/main/res/layout/item_place.xml b/app/src/main/res/layout/item_place.xml new file mode 100644 index 00000000..dadb8109 --- /dev/null +++ b/app/src/main/res/layout/item_place.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + \ 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..4d9bceab 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,6 @@ #FF000000 #FFFFFFFF + #1DCE9C + #e0e0e0 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 084ce9f6..688de004 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,7 @@ Map 검색 결과가 없습니다. 검색어를 입력해 주세요. + 카페1 + 서울 성동구 성수동 1 + 카페 \ No newline at end of file From 9339061e4f9a81ac35748acb140722b9c50ea87c Mon Sep 17 00:00:00 2001 From: changukLee Date: Thu, 4 Jul 2024 00:31:48 +0900 Subject: [PATCH 07/13] feat: update Save Search Word Feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검색 결과 목록에서 항목을 누르면 검색어 저장 목록에 추가되도록 함. --- .../campus/tech/kakao/map/MainActivity.kt | 189 ++++++++++++++---- .../map/data/SavedSearchWordRepository.kt | 52 +++++ .../tech/kakao/map/model/SavedSearchWord.kt | 5 + .../map/viewmodel/SavedSearchWordViewModel.kt | 43 ++++ app/src/main/res/layout/activity_main.xml | 24 ++- .../res/layout/item_saved_search_word.xml | 41 ++++ 6 files changed, 313 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt create mode 100644 app/src/main/res/layout/item_saved_search_word.xml 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 4aacb7a8..7317174b 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -12,27 +12,37 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import campus.tech.kakao.map.databinding.ActivityMainBinding import campus.tech.kakao.map.databinding.ItemPlaceBinding +import campus.tech.kakao.map.databinding.ItemSavedSearchWordBinding import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.model.SavedSearchWord import campus.tech.kakao.map.viewmodel.PlaceViewModel +import campus.tech.kakao.map.viewmodel.SavedSearchWordViewModel class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var placeViewModel: PlaceViewModel + private lateinit var savedSearchWordViewModel: SavedSearchWordViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) placeViewModel = ViewModelProvider(this)[PlaceViewModel::class.java] - binding.viewModel = placeViewModel + savedSearchWordViewModel = ViewModelProvider(this)[SavedSearchWordViewModel::class.java] + binding.placeViewModel = placeViewModel + binding.savedSearchWordViewModel = savedSearchWordViewModel binding.lifecycleOwner = this + setRecyclerViews() setSearchResultRecyclerView() - placeViewModel.clearAllPlaces() - testDataInsert() +// placeViewModel.clearAllPlaces() +// testDataInsert() setClearImageViewClickListener() setSearchEditText() + + observeViewModels() observeSearchResults() + observeSavedSearchWords() } /** @@ -64,15 +74,100 @@ class MainActivity : AppCompatActivity() { } } + /** + * RecyclerView들을 설정하는 함수. + */ + private fun setRecyclerViews() { + setSearchResultRecyclerView() + setSavedSearchWordRecyclerView() + } + + /** + * 검색 EditText가 변경되면 placeViewModel을 통해 검색을 수행하도록 하는 함수. + */ + private fun setSearchEditText() { + binding.searchEditText.addTextChangedListener { editable -> + val searchText = editable.toString().trim() + if (searchText.isNotEmpty()) { + placeViewModel.searchPlaces(searchText) + } else { + placeViewModel.searchPlaces("") + } + } + } + + /** + * clearImageView의 클릭 리스너를 설정하는 함수. + * + * searchEditText의 text를 null로 변경. + */ + private fun setClearImageViewClickListener() { + binding.searchClearImageView.setOnClickListener { + binding.searchEditText.text = null + } + } + + /** + * viewModel을 관찰하도록 하는 함수. + */ + private fun observeViewModels() { + observeSearchResults() + observeSavedSearchWords() + } + + + /** + * 검색 결과를 관찰하고, RecyclerView에 결과를 반영하는 함수. + */ + private fun observeSearchResults() { + placeViewModel.searchResults.observe( + this, + ) { places -> + if (places.isNotEmpty()) { + (binding.searchResultRecyclerView.adapter as ResultRecyclerViewAdapter).setPlaces( + places, + ) + } + } + } + + /** + * 저장된 검색어를 관찰하고, RecyclerView에 결과를 반영하는 함수. + */ + private fun observeSavedSearchWords() { + savedSearchWordViewModel.savedSearchWords.observe( + this, + ) { savedSearchWords -> + if (savedSearchWords.isNotEmpty()) { + (binding.savedSearchWordRecyclerView.adapter as SavedSearchWordRecyclerViewAdapter).setSavedSearchWords( + savedSearchWords, + ) + } + } + } + /** * 검색 결과를 표시하는 RecyclerView를 설정하는 함수. + * + * - `placeItemClickListener` : placeItem을 누르면 검색어가 저장되도록 하는 클릭 리스너 interface 구현 객체 */ private fun setSearchResultRecyclerView() { - binding.searchResultRecyclerView.adapter = ResultRecyclerViewAdapter() + val placeItemClickListener = + object : OnPlaceItemClickListener { + override fun onPlaceItemClicked(place: Place) { + savedSearchWordViewModel.insertSearchWord(SavedSearchWord(place.name)) + } + } + binding.searchResultRecyclerView.adapter = ResultRecyclerViewAdapter(placeItemClickListener) binding.searchResultRecyclerView.layoutManager = LinearLayoutManager(this) } - class ResultRecyclerViewAdapter : RecyclerView.Adapter() { + interface OnPlaceItemClickListener { + fun onPlaceItemClicked(place: Place) + } + + class ResultRecyclerViewAdapter(private val clickListener: OnPlaceItemClickListener) : + RecyclerView.Adapter() { private var searchResultList: List = emptyList() /** @@ -103,7 +198,11 @@ class MainActivity : AppCompatActivity() { holder: PlaceViewHolder, position: Int, ) { - holder.bind(searchResultList[position]) + val place = searchResultList[position] + holder.bind(place) + holder.itemView.setOnClickListener { + clickListener.onPlaceItemClicked(place) + } } class PlaceViewHolder(private val binding: ItemPlaceBinding) : @@ -115,42 +214,60 @@ class MainActivity : AppCompatActivity() { } /** - * 검색 EditText가 변경되면 placeViewModel을 통해 검색을 수행하도록 하는 함수. + * SavedSearchWordRecyclerView를 설정하는 함수. */ - private fun setSearchEditText() { - binding.searchEditText.addTextChangedListener { editable -> - val searchText = editable.toString().trim() - if (searchText.isNotEmpty()) { - placeViewModel.searchPlaces(searchText) - } else { - placeViewModel.searchPlaces("") - } - } + private fun setSavedSearchWordRecyclerView() { + binding.savedSearchWordRecyclerView.adapter = SavedSearchWordRecyclerViewAdapter() + binding.savedSearchWordRecyclerView.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) } - /** - * 검색 결과를 관찰하고, RecyclerView에 결과를 반영하는 함수. - */ - private fun observeSearchResults() { - placeViewModel.searchResults.observe( - this, - ) { places -> - if (places.isNotEmpty()) { - (binding.searchResultRecyclerView.adapter as ResultRecyclerViewAdapter).setPlaces( - places + class SavedSearchWordRecyclerViewAdapter : + RecyclerView.Adapter() { + private var savedSearchWordList: List = emptyList() + + /** + * RecyclerView에 표시할 저장된 검색어 리스트를 설정하는 함수. + * + * @param savedSearchWords 저장된 검색어 리스트 + */ + @SuppressLint("NotifyDataSetChanged") + fun setSavedSearchWords(savedSearchWords: List) { + savedSearchWordList = savedSearchWords + notifyDataSetChanged() + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): SavedSearchWordViewHolder { + val binding = + ItemSavedSearchWordBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, ) - } + return SavedSearchWordViewHolder(binding) } - } - /** - * clearImageView의 클릭 리스너를 설정하는 함수. - * - * searchEditText의 text를 null로 변경. - */ - private fun setClearImageViewClickListener() { - binding.clearImageView.setOnClickListener { - binding.searchEditText.text = null + override fun getItemCount(): Int { + return savedSearchWordList.size + } + + override fun onBindViewHolder( + holder: SavedSearchWordViewHolder, + position: Int, + ) { + val savedSearchWord = savedSearchWordList[position] + holder.bind(savedSearchWord) + } + + class SavedSearchWordViewHolder(private val binding: ItemSavedSearchWordBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(savedSearchWord: SavedSearchWord) { + binding.savedSearchWord = savedSearchWord + } } } + } diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt new file mode 100644 index 00000000..18067199 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt @@ -0,0 +1,52 @@ +package campus.tech.kakao.map.data + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import campus.tech.kakao.map.model.SavedSearchWord + +class SavedSearchWordRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COLUMN_NAME TEXT)") + } + + override fun onUpgrade( + db: SQLiteDatabase, + oldVersion: Int, + newVersion: Int, + ) { + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } + + fun insertSearchWord(searchWord: SavedSearchWord) { + val db = writableDatabase + val contentValues = + ContentValues().apply { + put(COLUMN_NAME, searchWord.name) + } + db.insert(TABLE_NAME, null, contentValues) + } + + fun getAllSearchWords(): List { + val db = readableDatabase + val cursor = db.query(TABLE_NAME, null, null, null, null, null, null) + val searchWords = mutableListOf() + while (cursor.moveToNext()) { + val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)) + searchWords.add(SavedSearchWord(name)) + } + cursor.close() + return searchWords + } + + companion object { + const val DATABASE_NAME = "search_words.db" + const val DATABASE_VERSION = 1 + const val TABLE_NAME = "search_words_data" + + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt b/app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt new file mode 100644 index 00000000..aa2a4f68 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.model + +data class SavedSearchWord( + val name: String, +) diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt new file mode 100644 index 00000000..97fcb4c4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt @@ -0,0 +1,43 @@ +package campus.tech.kakao.map.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.data.SavedSearchWordRepository +import campus.tech.kakao.map.model.SavedSearchWord +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SavedSearchWordViewModel(application: Application) : AndroidViewModel(application) { + private val repository: SavedSearchWordRepository = SavedSearchWordRepository(application) + private val _savedSearchWords = MutableLiveData>() + + val savedSearchWords: LiveData> get() = _savedSearchWords + + fun insertSearchWord(searchWord: SavedSearchWord) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repository.insertSearchWord(searchWord) + val currentList = _savedSearchWords.value ?: emptyList() + val updatedList = currentList + searchWord + _savedSearchWords.postValue(updatedList) + } + } + } + + private fun getAllSearchWords() { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val searchWords = repository.getAllSearchWords() + _savedSearchWords.postValue(searchWords) + } + } + } + + init { + getAllSearchWords() + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f07d57a3..407bf125 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,8 +8,13 @@ + + + + app:layout_constraintTop_toBottomOf="@id/search_edit_layout" + app:layout_constraintVertical_bias="1.0"> + + + android:visibility="@{placeViewModel.searchResults.isEmpty() ? View.GONE : View.VISIBLE}" /> + + + + + + + + + + + + + + + \ No newline at end of file From 9913f13fbf47680b9e2f8ed98647fb3499c1b5a5 Mon Sep 17 00:00:00 2001 From: changukLee Date: Thu, 4 Jul 2024 00:50:03 +0900 Subject: [PATCH 08/13] feat: update Delete Saved Search Word Feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clear 버튼을 누르면 해당 저장된 검색어가 목록에서 삭제되도록 구현 --- .../campus/tech/kakao/map/MainActivity.kt | 30 +++++++++++++++---- .../map/data/SavedSearchWordRepository.kt | 5 ++++ .../map/viewmodel/SavedSearchWordViewModel.kt | 11 +++++++ 3 files changed, 40 insertions(+), 6 deletions(-) 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 7317174b..1586a5c3 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -115,7 +115,6 @@ class MainActivity : AppCompatActivity() { observeSavedSearchWords() } - /** * 검색 결과를 관찰하고, RecyclerView에 결과를 반영하는 함수. */ @@ -213,16 +212,29 @@ class MainActivity : AppCompatActivity() { } } + interface OnSavedSearchWordClearImageViewClickListener { + fun onSavedSearchWordClearImageViewClicked(savedSearchWord: SavedSearchWord) + } + /** * SavedSearchWordRecyclerView를 설정하는 함수. + * + * - `savedSearchWordClearImageViewClickListener` : clear 버튼을 누르면 해당 저장된 검색어가 사라지도록 하는 클릭리스너 interface 구현 객체 */ private fun setSavedSearchWordRecyclerView() { - binding.savedSearchWordRecyclerView.adapter = SavedSearchWordRecyclerViewAdapter() + val savedSearchWordClearImageViewClickListener = + object : OnSavedSearchWordClearImageViewClickListener { + override fun onSavedSearchWordClearImageViewClicked(savedSearchWord: SavedSearchWord) { + savedSearchWordViewModel.deleteSearchWord(savedSearchWord) + } + } + binding.savedSearchWordRecyclerView.adapter = + SavedSearchWordRecyclerViewAdapter(savedSearchWordClearImageViewClickListener) binding.savedSearchWordRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) } - class SavedSearchWordRecyclerViewAdapter : + class SavedSearchWordRecyclerViewAdapter(private val clickListener: OnSavedSearchWordClearImageViewClickListener) : RecyclerView.Adapter() { private var savedSearchWordList: List = emptyList() @@ -247,7 +259,7 @@ class MainActivity : AppCompatActivity() { parent, false, ) - return SavedSearchWordViewHolder(binding) + return SavedSearchWordViewHolder(binding, clickListener) } override fun getItemCount(): Int { @@ -262,12 +274,18 @@ class MainActivity : AppCompatActivity() { holder.bind(savedSearchWord) } - class SavedSearchWordViewHolder(private val binding: ItemSavedSearchWordBinding) : + class SavedSearchWordViewHolder( + private val binding: ItemSavedSearchWordBinding, + private val clickListener: OnSavedSearchWordClearImageViewClickListener, + ) : RecyclerView.ViewHolder(binding.root) { fun bind(savedSearchWord: SavedSearchWord) { binding.savedSearchWord = savedSearchWord + + binding.savedSearchWordClearImageView.setOnClickListener { + clickListener.onSavedSearchWordClearImageViewClicked(savedSearchWord) + } } } } - } diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt index 18067199..052cb2eb 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt @@ -41,6 +41,11 @@ class SavedSearchWordRepository(context: Context) : SQLiteOpenHelper(context, DA return searchWords } + fun deleteSearchWord(searchWord: SavedSearchWord) { + val db = writableDatabase + db.delete(TABLE_NAME, "$COLUMN_NAME = ?", arrayOf(searchWord.name)) + } + companion object { const val DATABASE_NAME = "search_words.db" const val DATABASE_VERSION = 1 diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt index 97fcb4c4..3d35d99a 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt @@ -28,6 +28,17 @@ class SavedSearchWordViewModel(application: Application) : AndroidViewModel(appl } } + fun deleteSearchWord(searchWord: SavedSearchWord) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repository.deleteSearchWord(searchWord) + val currentList = _savedSearchWords.value ?: emptyList() + val updatedList = currentList.filter { it != searchWord } + _savedSearchWords.postValue(updatedList) + } + } + } + private fun getAllSearchWords() { viewModelScope.launch { withContext(Dispatchers.IO) { From beb45f74ecaf85714ee313121e31b267e38c5e81 Mon Sep 17 00:00:00 2001 From: changukLee Date: Thu, 4 Jul 2024 01:10:30 +0900 Subject: [PATCH 09/13] docs: update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기능 리스트에 기능 추가 --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae76538b..135a395e 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,12 @@ 1. 검색어를 입력하면 검색 결과 목록이 표시된다. - 리사이클러뷰 사용(세로 스크롤) -2. 입력한 검색어는 X를 누럴서 삭제할 수 있다. +2. 입력한 검색어는 X를 눌러서 삭제할 수 있다. 3. 검색 결과 목록에서 하나의 항목을 선택할 수 있다. - 선택된 항목은 검색어 저장 목록에 추가된다. - 리사이클러뷰 사용(가로 스크롤) -4. 저장된 검색어는 X를 눌러서 삭제할 수 있다. \ No newline at end of file +4. 저장된 검색어는 X를 눌러서 삭제할 수 있다. + +5. 이미 검색어 저장 목록에 있는 검색어를 검색 결과 목록에서 선택한 경우 기존 검색어는 삭제하고 다시 추가한다. \ No newline at end of file From b88e8367acdd12f0d56821081ce8aeba33aa330e Mon Sep 17 00:00:00 2001 From: changukLee Date: Thu, 4 Jul 2024 01:11:34 +0900 Subject: [PATCH 10/13] feat: update Delete And Insert Existing Search Word Feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검색 결과에서 선택한 검색어가 이미 저장된 목록에 있을 경우, 기존 검색어를 삭제하고 다시 추가하도록 구현 --- .../campus/tech/kakao/map/MainActivity.kt | 12 ++++----- .../map/data/SavedSearchWordRepository.kt | 25 +++++++++++++------ .../map/viewmodel/SavedSearchWordViewModel.kt | 6 ++--- 3 files changed, 26 insertions(+), 17 deletions(-) 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 1586a5c3..f7fca4a0 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -34,8 +34,8 @@ class MainActivity : AppCompatActivity() { setRecyclerViews() setSearchResultRecyclerView() -// placeViewModel.clearAllPlaces() -// testDataInsert() + placeViewModel.clearAllPlaces() + testDataInsert() setClearImageViewClickListener() setSearchEditText() @@ -145,6 +145,10 @@ class MainActivity : AppCompatActivity() { } } + interface OnPlaceItemClickListener { + fun onPlaceItemClicked(place: Place) + } + /** * 검색 결과를 표시하는 RecyclerView를 설정하는 함수. * @@ -161,10 +165,6 @@ class MainActivity : AppCompatActivity() { binding.searchResultRecyclerView.layoutManager = LinearLayoutManager(this) } - interface OnPlaceItemClickListener { - fun onPlaceItemClicked(place: Place) - } - class ResultRecyclerViewAdapter(private val clickListener: OnPlaceItemClickListener) : RecyclerView.Adapter() { private var searchResultList: List = emptyList() diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt index 052cb2eb..2def35dc 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt @@ -4,9 +4,11 @@ import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper +import android.util.Log import campus.tech.kakao.map.model.SavedSearchWord -class SavedSearchWordRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { +class SavedSearchWordRepository(context: Context) : + SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { override fun onCreate(db: SQLiteDatabase) { db.execSQL("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COLUMN_NAME TEXT)") } @@ -20,13 +22,22 @@ class SavedSearchWordRepository(context: Context) : SQLiteOpenHelper(context, DA onCreate(db) } - fun insertSearchWord(searchWord: SavedSearchWord) { + fun deleteAndInsertSearchWord(searchWord: SavedSearchWord) { val db = writableDatabase - val contentValues = - ContentValues().apply { - put(COLUMN_NAME, searchWord.name) - } - db.insert(TABLE_NAME, null, contentValues) + db.beginTransaction() + try { + db.delete(TABLE_NAME, "$COLUMN_NAME = ?", arrayOf(searchWord.name)) + val contentValues = + ContentValues().apply { + put(COLUMN_NAME, searchWord.name) + } + db.insert(TABLE_NAME, null, contentValues) + db.setTransactionSuccessful() + } catch (e: Exception) { + Log.e("dbError", "Error while delete and insert: ${e.message}") + } finally { + db.endTransaction() + } } fun getAllSearchWords(): List { diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt index 3d35d99a..2cf16d19 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt @@ -20,10 +20,8 @@ class SavedSearchWordViewModel(application: Application) : AndroidViewModel(appl fun insertSearchWord(searchWord: SavedSearchWord) { viewModelScope.launch { withContext(Dispatchers.IO) { - repository.insertSearchWord(searchWord) - val currentList = _savedSearchWords.value ?: emptyList() - val updatedList = currentList + searchWord - _savedSearchWords.postValue(updatedList) + repository.deleteAndInsertSearchWord(searchWord) + getAllSearchWords() } } } From 5cf713bba54c3ee669bd500b67b2445daecce704 Mon Sep 17 00:00:00 2001 From: changukLee Date: Thu, 4 Jul 2024 11:20:38 +0900 Subject: [PATCH 11/13] feat: update Store And Delete Search Word Using PlaceId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이름이 같은 장소가 있을 수 있기 때문에 placeId를 참조하여 저장 및 삭제하도록 구현 --- .../campus/tech/kakao/map/MainActivity.kt | 14 +++++++--- .../tech/kakao/map/data/PlaceRepository.kt | 7 +++-- .../map/data/SavedSearchWordRepository.kt | 28 ++++++++++++++++--- .../java/campus/tech/kakao/map/model/Place.kt | 1 + .../tech/kakao/map/model/SavedSearchWord.kt | 2 ++ 5 files changed, 41 insertions(+), 11 deletions(-) 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 f7fca4a0..bfc1c597 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -51,6 +51,7 @@ class MainActivity : AppCompatActivity() { private fun testDataInsert() { insertPlaces("서울 성동구 성수동", "카페") insertPlaces("서울 강남구 대치동", "약국") + insertPlaces("서울 강남구 수서동", "약국") } /** @@ -66,9 +67,9 @@ class MainActivity : AppCompatActivity() { repeat(20) { idx -> placeViewModel.insertPlace( Place( - category + (idx + 1), - "$address ${idx + 1}", - category, + name = category + (idx + 1), + address = "$address ${idx + 1}", + category = category, ), ) } @@ -158,7 +159,12 @@ class MainActivity : AppCompatActivity() { val placeItemClickListener = object : OnPlaceItemClickListener { override fun onPlaceItemClicked(place: Place) { - savedSearchWordViewModel.insertSearchWord(SavedSearchWord(place.name)) + savedSearchWordViewModel.insertSearchWord( + SavedSearchWord( + name = place.name, + placeId = place.id, + ), + ) } } binding.searchResultRecyclerView.adapter = ResultRecyclerViewAdapter(placeItemClickListener) diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt index d5841d54..fe24ffd9 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt @@ -42,9 +42,10 @@ class PlaceRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAM val cursor = db.query(TABLE_NAME, null, "category=?", arrayOf(category), null, null, null) val places = mutableListOf() while (cursor.moveToNext()) { - val name = cursor.getString(cursor.getColumnIndexOrThrow("name")) - val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) - places.add(Place(name, address, category)) + val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) + val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)) + val address = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ADDRESS)) + places.add(Place(id, name, address, category)) } cursor.close() return places diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt index 2def35dc..2fef332f 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt @@ -10,7 +10,14 @@ import campus.tech.kakao.map.model.SavedSearchWord class SavedSearchWordRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { override fun onCreate(db: SQLiteDatabase) { - db.execSQL("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, $COLUMN_NAME TEXT)") + db.execSQL( + "CREATE TABLE $TABLE_NAME (" + + "$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$COLUMN_NAME TEXT, " + + "$COLUMN_PLACE_ID INTEGER, " + + "FOREIGN KEY($COLUMN_PLACE_ID) REFERENCES ${PlaceRepository.TABLE_NAME}(${PlaceRepository.COLUMN_ID}) ON DELETE CASCADE" + + ")", + ) } override fun onUpgrade( @@ -26,10 +33,14 @@ class SavedSearchWordRepository(context: Context) : val db = writableDatabase db.beginTransaction() try { - db.delete(TABLE_NAME, "$COLUMN_NAME = ?", arrayOf(searchWord.name)) + db.delete(TABLE_NAME, "$COLUMN_PLACE_ID = ?", arrayOf(searchWord.placeId.toString())) val contentValues = ContentValues().apply { put(COLUMN_NAME, searchWord.name) + put( + COLUMN_PLACE_ID, + searchWord.placeId, + ) } db.insert(TABLE_NAME, null, contentValues) db.setTransactionSuccessful() @@ -45,8 +56,10 @@ class SavedSearchWordRepository(context: Context) : val cursor = db.query(TABLE_NAME, null, null, null, null, null, null) val searchWords = mutableListOf() while (cursor.moveToNext()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)) - searchWords.add(SavedSearchWord(name)) + val placeId = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_PLACE_ID)) + searchWords.add(SavedSearchWord(id, name, placeId)) } cursor.close() return searchWords @@ -54,7 +67,13 @@ class SavedSearchWordRepository(context: Context) : fun deleteSearchWord(searchWord: SavedSearchWord) { val db = writableDatabase - db.delete(TABLE_NAME, "$COLUMN_NAME = ?", arrayOf(searchWord.name)) + db.delete(TABLE_NAME, "$COLUMN_ID = ?", arrayOf(searchWord.id.toString())) + } + + fun clearAll() { + val db = writableDatabase + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) } companion object { @@ -64,5 +83,6 @@ class SavedSearchWordRepository(context: Context) : const val COLUMN_ID = "id" const val COLUMN_NAME = "name" + const val COLUMN_PLACE_ID = "place_id" } } diff --git a/app/src/main/java/campus/tech/kakao/map/model/Place.kt b/app/src/main/java/campus/tech/kakao/map/model/Place.kt index de84f7e0..486ec57e 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/Place.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/Place.kt @@ -1,6 +1,7 @@ package campus.tech.kakao.map.model data class Place( + val id: Long = 0, val name: String, val address: String? = null, val category: String? = null, diff --git a/app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt b/app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt index aa2a4f68..eff445de 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/SavedSearchWord.kt @@ -1,5 +1,7 @@ package campus.tech.kakao.map.model data class SavedSearchWord( + val id: Long = 0, val name: String, + val placeId: Long, ) From 13e9f79b5a7b3406780cf708ed7d6c21d6f5dd53 Mon Sep 17 00:00:00 2001 From: changukLee Date: Thu, 4 Jul 2024 12:13:05 +0900 Subject: [PATCH 12/13] refactor: improve Code Readability And Remove Duplicate Code --- .../campus/tech/kakao/map/MainActivity.kt | 132 ++++++++++-------- .../map/data/SavedSearchWordRepository.kt | 32 ++--- .../kakao/map/viewmodel/PlaceViewModel.kt | 10 +- .../map/viewmodel/SavedSearchWordViewModel.kt | 17 ++- 4 files changed, 99 insertions(+), 92 deletions(-) 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 bfc1c597..2fc7fc89 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -20,29 +20,40 @@ import campus.tech.kakao.map.viewmodel.SavedSearchWordViewModel class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding - private lateinit var placeViewModel: PlaceViewModel - private lateinit var savedSearchWordViewModel: SavedSearchWordViewModel + private val placeViewModel by lazy { ViewModelProvider(this)[PlaceViewModel::class.java] } + private val savedSearchWordViewModel by lazy { ViewModelProvider(this)[SavedSearchWordViewModel::class.java] } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setupBinding() + setupDummyData() + setupViews() + observeViewModels() + } + + private fun setupBinding() { binding = DataBindingUtil.setContentView(this, R.layout.activity_main) - placeViewModel = ViewModelProvider(this)[PlaceViewModel::class.java] - savedSearchWordViewModel = ViewModelProvider(this)[SavedSearchWordViewModel::class.java] - binding.placeViewModel = placeViewModel - binding.savedSearchWordViewModel = savedSearchWordViewModel - binding.lifecycleOwner = this + binding.apply { + this.placeViewModel = this@MainActivity.placeViewModel + this.savedSearchWordViewModel = this@MainActivity.savedSearchWordViewModel + this.lifecycleOwner = this@MainActivity + } + } - setRecyclerViews() + /** + * RecyclerView들을 설정하는 함수. + */ + private fun setupRecyclerViews() { setSearchResultRecyclerView() + setSavedSearchWordRecyclerView() + } + + /** + * 테스트용 더미 데이터를 place db에 삽입하기 위한 함수. + */ + private fun setupDummyData() { placeViewModel.clearAllPlaces() testDataInsert() - - setClearImageViewClickListener() - setSearchEditText() - - observeViewModels() - observeSearchResults() - observeSavedSearchWords() } /** @@ -76,11 +87,12 @@ class MainActivity : AppCompatActivity() { } /** - * RecyclerView들을 설정하는 함수. + * view들에 필요한 작업을 처리하는 함수. */ - private fun setRecyclerViews() { - setSearchResultRecyclerView() - setSavedSearchWordRecyclerView() + private fun setupViews() { + setClearImageViewClickListener() + setSearchEditText() + setupRecyclerViews() } /** @@ -90,9 +102,9 @@ class MainActivity : AppCompatActivity() { binding.searchEditText.addTextChangedListener { editable -> val searchText = editable.toString().trim() if (searchText.isNotEmpty()) { - placeViewModel.searchPlaces(searchText) + placeViewModel.searchPlacesByCategory(searchText) } else { - placeViewModel.searchPlaces("") + placeViewModel.searchPlacesByCategory("") } } } @@ -108,44 +120,6 @@ class MainActivity : AppCompatActivity() { } } - /** - * viewModel을 관찰하도록 하는 함수. - */ - private fun observeViewModels() { - observeSearchResults() - observeSavedSearchWords() - } - - /** - * 검색 결과를 관찰하고, RecyclerView에 결과를 반영하는 함수. - */ - private fun observeSearchResults() { - placeViewModel.searchResults.observe( - this, - ) { places -> - if (places.isNotEmpty()) { - (binding.searchResultRecyclerView.adapter as ResultRecyclerViewAdapter).setPlaces( - places, - ) - } - } - } - - /** - * 저장된 검색어를 관찰하고, RecyclerView에 결과를 반영하는 함수. - */ - private fun observeSavedSearchWords() { - savedSearchWordViewModel.savedSearchWords.observe( - this, - ) { savedSearchWords -> - if (savedSearchWords.isNotEmpty()) { - (binding.savedSearchWordRecyclerView.adapter as SavedSearchWordRecyclerViewAdapter).setSavedSearchWords( - savedSearchWords, - ) - } - } - } - interface OnPlaceItemClickListener { fun onPlaceItemClicked(place: Place) } @@ -231,7 +205,7 @@ class MainActivity : AppCompatActivity() { val savedSearchWordClearImageViewClickListener = object : OnSavedSearchWordClearImageViewClickListener { override fun onSavedSearchWordClearImageViewClicked(savedSearchWord: SavedSearchWord) { - savedSearchWordViewModel.deleteSearchWord(savedSearchWord) + savedSearchWordViewModel.deleteSearchWordById(savedSearchWord) } } binding.savedSearchWordRecyclerView.adapter = @@ -294,4 +268,42 @@ class MainActivity : AppCompatActivity() { } } } + + /** + * viewModel을 관찰하도록 하는 함수. + */ + private fun observeViewModels() { + observeSearchResults() + observeSavedSearchWords() + } + + /** + * 검색 결과를 관찰하고, RecyclerView에 결과를 반영하는 함수. + */ + private fun observeSearchResults() { + placeViewModel.searchResults.observe( + this, + ) { places -> + if (places.isNotEmpty()) { + (binding.searchResultRecyclerView.adapter as ResultRecyclerViewAdapter).setPlaces( + places, + ) + } + } + } + + /** + * 저장된 검색어를 관찰하고, RecyclerView에 결과를 반영하는 함수. + */ + private fun observeSavedSearchWords() { + savedSearchWordViewModel.savedSearchWords.observe( + this, + ) { savedSearchWords -> + if (savedSearchWords.isNotEmpty()) { + (binding.savedSearchWordRecyclerView.adapter as SavedSearchWordRecyclerViewAdapter).setSavedSearchWords( + savedSearchWords, + ) + } + } + } } diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt index 2fef332f..8bf8eaab 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt @@ -10,14 +10,15 @@ import campus.tech.kakao.map.model.SavedSearchWord class SavedSearchWordRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { override fun onCreate(db: SQLiteDatabase) { - db.execSQL( - "CREATE TABLE $TABLE_NAME (" + - "$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, " + - "$COLUMN_NAME TEXT, " + - "$COLUMN_PLACE_ID INTEGER, " + - "FOREIGN KEY($COLUMN_PLACE_ID) REFERENCES ${PlaceRepository.TABLE_NAME}(${PlaceRepository.COLUMN_ID}) ON DELETE CASCADE" + - ")", - ) + val createTableQuery = """ + CREATE TABLE $TABLE_NAME ( + $COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, + $COLUMN_NAME TEXT, + $COLUMN_PLACE_ID INTEGER, + FOREIGN KEY($COLUMN_PLACE_ID) REFERENCES ${PlaceRepository.TABLE_NAME}(${PlaceRepository.COLUMN_ID}) ON DELETE CASCADE + ) + """ + db.execSQL(createTableQuery) } override fun onUpgrade( @@ -29,7 +30,7 @@ class SavedSearchWordRepository(context: Context) : onCreate(db) } - fun deleteAndInsertSearchWord(searchWord: SavedSearchWord) { + fun insertOrUpdateSearchWord(searchWord: SavedSearchWord) { val db = writableDatabase db.beginTransaction() try { @@ -65,15 +66,10 @@ class SavedSearchWordRepository(context: Context) : return searchWords } - fun deleteSearchWord(searchWord: SavedSearchWord) { - val db = writableDatabase - db.delete(TABLE_NAME, "$COLUMN_ID = ?", arrayOf(searchWord.id.toString())) - } - - fun clearAll() { - val db = writableDatabase - db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") - onCreate(db) + fun deleteSearchWordById(id: Long) { + writableDatabase.use { db -> + db.delete(TABLE_NAME, "$COLUMN_ID = ?", arrayOf(id.toString())) + } } companion object { diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt index 32d1e9fa..70c8895b 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt @@ -17,6 +17,10 @@ class PlaceViewModel(application: Application) : AndroidViewModel(application) { val searchResults: LiveData> get() = _searchResults + init { + clearSearchResults() + } + fun insertPlace(place: Place) { placeRepository.insertPlace(place) } @@ -29,7 +33,7 @@ class PlaceViewModel(application: Application) : AndroidViewModel(application) { _searchResults.value = emptyList() } - fun searchPlaces(category: String) { + fun searchPlacesByCategory(category: String) { viewModelScope.launch { withContext(Dispatchers.IO) { val results = placeRepository.getPlacesByCategory(category) @@ -37,8 +41,4 @@ class PlaceViewModel(application: Application) : AndroidViewModel(application) { } } } - - init { - clearSearchResults() - } } diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt index 2cf16d19..c65d4d10 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt @@ -17,22 +17,25 @@ class SavedSearchWordViewModel(application: Application) : AndroidViewModel(appl val savedSearchWords: LiveData> get() = _savedSearchWords + init { + getAllSearchWords() + } + fun insertSearchWord(searchWord: SavedSearchWord) { viewModelScope.launch { withContext(Dispatchers.IO) { - repository.deleteAndInsertSearchWord(searchWord) + repository.insertOrUpdateSearchWord(searchWord) getAllSearchWords() } } } - fun deleteSearchWord(searchWord: SavedSearchWord) { + fun deleteSearchWordById(searchWord: SavedSearchWord) { viewModelScope.launch { withContext(Dispatchers.IO) { - repository.deleteSearchWord(searchWord) + repository.deleteSearchWordById(searchWord.id) val currentList = _savedSearchWords.value ?: emptyList() - val updatedList = currentList.filter { it != searchWord } - _savedSearchWords.postValue(updatedList) + _savedSearchWords.postValue(currentList - searchWord) } } } @@ -45,8 +48,4 @@ class SavedSearchWordViewModel(application: Application) : AndroidViewModel(appl } } } - - init { - getAllSearchWords() - } } From 0a87ac83c2442e7a6b54c5f52a876d7d2246c1d7 Mon Sep 17 00:00:00 2001 From: changukLee Date: Mon, 8 Jul 2024 15:14:14 +0900 Subject: [PATCH 13/13] refactor: refactor Suggested Improvements from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - repository에 context를 직접적으로 넘겨주지 않게 변경 - 리사이클러 뷰에서 notifydatasetchanged() 사용하지 않도록 변경 --- .../campus/tech/kakao/map/MainActivity.kt | 128 +++++++----------- .../tech/kakao/map/data/PlaceDBHelper.kt | 81 +++++++++++ .../tech/kakao/map/data/PlaceRepository.kt | 57 +------- .../kakao/map/data/SavedSearchWordDBHelper.kt | 81 +++++++++++ .../map/data/SavedSearchWordRepository.kt | 75 +--------- .../kakao/map/viewmodel/PlaceViewModel.kt | 16 +-- .../map/viewmodel/SavedSearchWordViewModel.kt | 33 ++--- 7 files changed, 243 insertions(+), 228 deletions(-) create mode 100644 app/src/main/java/campus/tech/kakao/map/data/PlaceDBHelper.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordDBHelper.kt 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 2fc7fc89..3c11b9f1 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -1,6 +1,5 @@ package campus.tech.kakao.map -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup @@ -8,7 +7,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.addTextChangedListener import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import campus.tech.kakao.map.databinding.ActivityMainBinding import campus.tech.kakao.map.databinding.ItemPlaceBinding @@ -78,7 +79,7 @@ class MainActivity : AppCompatActivity() { repeat(20) { idx -> placeViewModel.insertPlace( Place( - name = category + (idx + 1), + name = "$category ${idx + 1}", address = "$address ${idx + 1}", category = category, ), @@ -101,11 +102,7 @@ class MainActivity : AppCompatActivity() { private fun setSearchEditText() { binding.searchEditText.addTextChangedListener { editable -> val searchText = editable.toString().trim() - if (searchText.isNotEmpty()) { - placeViewModel.searchPlacesByCategory(searchText) - } else { - placeViewModel.searchPlacesByCategory("") - } + placeViewModel.searchPlacesByCategory(searchText) } } @@ -146,48 +143,42 @@ class MainActivity : AppCompatActivity() { } class ResultRecyclerViewAdapter(private val clickListener: OnPlaceItemClickListener) : - RecyclerView.Adapter() { - private var searchResultList: List = emptyList() - - /** - * RecyclerView에 표시할 장소 리스트를 설정하는 함수. - * - * @param places 장소 리스트 - */ - @SuppressLint("NotifyDataSetChanged") - fun setPlaces(places: List) { - searchResultList = places - notifyDataSetChanged() - } - + ListAdapter(PlaceDiffCallback()) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, ): PlaceViewHolder { - val binding = - ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = ItemPlaceBinding.inflate(LayoutInflater.from(parent.context), parent, false) return PlaceViewHolder(binding) } - override fun getItemCount(): Int { - return searchResultList.size - } - override fun onBindViewHolder( holder: PlaceViewHolder, position: Int, ) { - val place = searchResultList[position] - holder.bind(place) + val place = getItem(position) + + holder.binding.place = place holder.itemView.setOnClickListener { clickListener.onPlaceItemClicked(place) } } - class PlaceViewHolder(private val binding: ItemPlaceBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(place: Place) { - binding.place = place + class PlaceViewHolder(val binding: ItemPlaceBinding) : RecyclerView.ViewHolder(binding.root) + + private class PlaceDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Place, + newItem: Place, + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: Place, + newItem: Place, + ): Boolean { + return oldItem == newItem } } } @@ -215,56 +206,43 @@ class MainActivity : AppCompatActivity() { } class SavedSearchWordRecyclerViewAdapter(private val clickListener: OnSavedSearchWordClearImageViewClickListener) : - RecyclerView.Adapter() { - private var savedSearchWordList: List = emptyList() - - /** - * RecyclerView에 표시할 저장된 검색어 리스트를 설정하는 함수. - * - * @param savedSearchWords 저장된 검색어 리스트 - */ - @SuppressLint("NotifyDataSetChanged") - fun setSavedSearchWords(savedSearchWords: List) { - savedSearchWordList = savedSearchWords - notifyDataSetChanged() - } - + ListAdapter(SavedSearchWordDiffCallback()) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, ): SavedSearchWordViewHolder { - val binding = - ItemSavedSearchWordBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ) - return SavedSearchWordViewHolder(binding, clickListener) - } - - override fun getItemCount(): Int { - return savedSearchWordList.size + val binding = ItemSavedSearchWordBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SavedSearchWordViewHolder(binding) } override fun onBindViewHolder( holder: SavedSearchWordViewHolder, position: Int, ) { - val savedSearchWord = savedSearchWordList[position] - holder.bind(savedSearchWord) + val savedSearchWord = getItem(position) + holder.binding.savedSearchWord = savedSearchWord + holder.itemView.setOnClickListener { + clickListener.onSavedSearchWordClearImageViewClicked(savedSearchWord) + } } class SavedSearchWordViewHolder( - private val binding: ItemSavedSearchWordBinding, - private val clickListener: OnSavedSearchWordClearImageViewClickListener, - ) : - RecyclerView.ViewHolder(binding.root) { - fun bind(savedSearchWord: SavedSearchWord) { - binding.savedSearchWord = savedSearchWord + val binding: ItemSavedSearchWordBinding, + ) : RecyclerView.ViewHolder(binding.root) - binding.savedSearchWordClearImageView.setOnClickListener { - clickListener.onSavedSearchWordClearImageViewClicked(savedSearchWord) - } + private class SavedSearchWordDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: SavedSearchWord, + newItem: SavedSearchWord, + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: SavedSearchWord, + newItem: SavedSearchWord, + ): Boolean { + return oldItem == newItem } } } @@ -284,11 +262,7 @@ class MainActivity : AppCompatActivity() { placeViewModel.searchResults.observe( this, ) { places -> - if (places.isNotEmpty()) { - (binding.searchResultRecyclerView.adapter as ResultRecyclerViewAdapter).setPlaces( - places, - ) - } + (binding.searchResultRecyclerView.adapter as? ResultRecyclerViewAdapter)?.submitList(places) } } @@ -299,11 +273,7 @@ class MainActivity : AppCompatActivity() { savedSearchWordViewModel.savedSearchWords.observe( this, ) { savedSearchWords -> - if (savedSearchWords.isNotEmpty()) { - (binding.savedSearchWordRecyclerView.adapter as SavedSearchWordRecyclerViewAdapter).setSavedSearchWords( - savedSearchWords, - ) - } + (binding.savedSearchWordRecyclerView.adapter as? SavedSearchWordRecyclerViewAdapter)?.submitList(savedSearchWords) } } } diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceDBHelper.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceDBHelper.kt new file mode 100644 index 00000000..1fe370ee --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceDBHelper.kt @@ -0,0 +1,81 @@ +package campus.tech.kakao.map.data + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import campus.tech.kakao.map.model.Place + +class PlaceDBHelper(context: Context) : + SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + override fun onCreate(db: SQLiteDatabase) { + val createTableQuery = + "CREATE TABLE $TABLE_NAME (" + + "$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$COLUMN_NAME TEXT, " + + "$COLUMN_ADDRESS TEXT, " + + "$COLUMN_CATEGORY TEXT" + + ")" + db.execSQL(createTableQuery) + } + + override fun onUpgrade( + db: SQLiteDatabase, + oldVersion: Int, + newVersion: Int, + ) { + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } + + fun insertPlace(place: Place) { + val db = writableDatabase + val contentValues = + ContentValues().apply { + put(COLUMN_NAME, place.name) + put(COLUMN_ADDRESS, place.address) + put(COLUMN_CATEGORY, place.category) + } + db.insert(TABLE_NAME, null, contentValues) + } + + fun clearAllPlaces() { + val db = writableDatabase + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } + + fun getPlacesByCategory(category: String): List { + val db = readableDatabase + val cursor = + db.query( + TABLE_NAME, + null, + "$COLUMN_CATEGORY=?", + arrayOf(category), + null, + null, + null, + ) + val places = mutableListOf() + while (cursor.moveToNext()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) + val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)) + val address = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ADDRESS)) + places.add(Place(id, name, address, category)) + } + cursor.close() + return places + } + + companion object { + const val DATABASE_NAME = "place.db" + const val DATABASE_VERSION = 1 + const val TABLE_NAME = "place_data" + + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_ADDRESS = "address" + const val COLUMN_CATEGORY = "category" + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt index fe24ffd9..c3d2170c 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt @@ -1,64 +1,19 @@ package campus.tech.kakao.map.data -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper import campus.tech.kakao.map.model.Place -class PlaceRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { - override fun onCreate(db: SQLiteDatabase) { - db.execSQL("CREATE TABLE $TABLE_NAME (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, category TEXT)") - } - - override fun onUpgrade( - db: SQLiteDatabase, - oldVersion: Int, - newVersion: Int, - ) { - db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") - onCreate(db) - } - +class PlaceRepository( + private val dbHelper: PlaceDBHelper, +) { fun insertPlace(place: Place) { - val db = writableDatabase - val contentValues = - ContentValues().apply { - put(COLUMN_NAME, place.name) - put(COLUMN_ADDRESS, place.address) - put(COLUMN_CATEGORY, place.category) - } - db.insert(TABLE_NAME, null, contentValues) + dbHelper.insertPlace(place) } fun clearAllPlaces() { - val db = writableDatabase - db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") - onCreate(db) + dbHelper.clearAllPlaces() } fun getPlacesByCategory(category: String): List { - val db = readableDatabase - val cursor = db.query(TABLE_NAME, null, "category=?", arrayOf(category), null, null, null) - val places = mutableListOf() - while (cursor.moveToNext()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) - val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)) - val address = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ADDRESS)) - places.add(Place(id, name, address, category)) - } - cursor.close() - return places - } - - companion object { - const val DATABASE_NAME = "place.db" - const val DATABASE_VERSION = 1 - const val TABLE_NAME = "place_data" - - const val COLUMN_ID = "id" - const val COLUMN_NAME = "name" - const val COLUMN_ADDRESS = "address" - const val COLUMN_CATEGORY = "category" + return dbHelper.getPlacesByCategory(category) } } diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordDBHelper.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordDBHelper.kt new file mode 100644 index 00000000..492cbdc6 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordDBHelper.kt @@ -0,0 +1,81 @@ +package campus.tech.kakao.map.data + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log +import campus.tech.kakao.map.model.SavedSearchWord + +class SavedSearchWordDBHelper(context: Context) : + SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + override fun onCreate(db: SQLiteDatabase) { + val createTableQuery = """ + CREATE TABLE $TABLE_NAME ( + $COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, + $COLUMN_NAME TEXT, + $COLUMN_PLACE_ID INTEGER, + FOREIGN KEY($COLUMN_PLACE_ID) REFERENCES ${PlaceDBHelper.TABLE_NAME}(${PlaceDBHelper.COLUMN_ID}) ON DELETE CASCADE + ) + """ + db.execSQL(createTableQuery) + } + + override fun onUpgrade( + db: SQLiteDatabase, + oldVersion: Int, + newVersion: Int, + ) { + db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") + onCreate(db) + } + + fun insertOrUpdateSearchWord(searchWord: SavedSearchWord) { + val db = writableDatabase + db.beginTransaction() + try { + db.delete(TABLE_NAME, "$COLUMN_PLACE_ID = ?", arrayOf(searchWord.placeId.toString())) + val contentValues = + ContentValues().apply { + put(COLUMN_NAME, searchWord.name) + put(COLUMN_PLACE_ID, searchWord.placeId) + } + db.insert(TABLE_NAME, null, contentValues) + db.setTransactionSuccessful() + } catch (e: Exception) { + Log.e("dbError", "Error while delete and insert: ${e.message}") + } finally { + db.endTransaction() + } + } + + fun getAllSearchWords(): List { + val db = readableDatabase + val cursor = db.query(TABLE_NAME, null, null, null, null, null, null) + val searchWords = mutableListOf() + while (cursor.moveToNext()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) + val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)) + val placeId = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_PLACE_ID)) + searchWords.add(SavedSearchWord(id, name, placeId)) + } + cursor.close() + return searchWords + } + + fun deleteSearchWordById(id: Long) { + writableDatabase.use { db -> + db.delete(TABLE_NAME, "$COLUMN_ID = ?", arrayOf(id.toString())) + } + } + + companion object { + const val DATABASE_NAME = "search_words.db" + const val DATABASE_VERSION = 1 + const val TABLE_NAME = "search_words_data" + + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_PLACE_ID = "place_id" + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt index 8bf8eaab..b5fc83d2 100644 --- a/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedSearchWordRepository.kt @@ -1,84 +1,17 @@ package campus.tech.kakao.map.data -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.util.Log import campus.tech.kakao.map.model.SavedSearchWord -class SavedSearchWordRepository(context: Context) : - SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { - override fun onCreate(db: SQLiteDatabase) { - val createTableQuery = """ - CREATE TABLE $TABLE_NAME ( - $COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, - $COLUMN_NAME TEXT, - $COLUMN_PLACE_ID INTEGER, - FOREIGN KEY($COLUMN_PLACE_ID) REFERENCES ${PlaceRepository.TABLE_NAME}(${PlaceRepository.COLUMN_ID}) ON DELETE CASCADE - ) - """ - db.execSQL(createTableQuery) - } - - override fun onUpgrade( - db: SQLiteDatabase, - oldVersion: Int, - newVersion: Int, - ) { - db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME") - onCreate(db) - } - +class SavedSearchWordRepository(private val dbHelper: SavedSearchWordDBHelper) { fun insertOrUpdateSearchWord(searchWord: SavedSearchWord) { - val db = writableDatabase - db.beginTransaction() - try { - db.delete(TABLE_NAME, "$COLUMN_PLACE_ID = ?", arrayOf(searchWord.placeId.toString())) - val contentValues = - ContentValues().apply { - put(COLUMN_NAME, searchWord.name) - put( - COLUMN_PLACE_ID, - searchWord.placeId, - ) - } - db.insert(TABLE_NAME, null, contentValues) - db.setTransactionSuccessful() - } catch (e: Exception) { - Log.e("dbError", "Error while delete and insert: ${e.message}") - } finally { - db.endTransaction() - } + dbHelper.insertOrUpdateSearchWord(searchWord) } fun getAllSearchWords(): List { - val db = readableDatabase - val cursor = db.query(TABLE_NAME, null, null, null, null, null, null) - val searchWords = mutableListOf() - while (cursor.moveToNext()) { - val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) - val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)) - val placeId = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_PLACE_ID)) - searchWords.add(SavedSearchWord(id, name, placeId)) - } - cursor.close() - return searchWords + return dbHelper.getAllSearchWords() } fun deleteSearchWordById(id: Long) { - writableDatabase.use { db -> - db.delete(TABLE_NAME, "$COLUMN_ID = ?", arrayOf(id.toString())) - } - } - - companion object { - const val DATABASE_NAME = "search_words.db" - const val DATABASE_VERSION = 1 - const val TABLE_NAME = "search_words_data" - - const val COLUMN_ID = "id" - const val COLUMN_NAME = "name" - const val COLUMN_PLACE_ID = "place_id" + dbHelper.deleteSearchWordById(id) } } diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt index 70c8895b..3e851c40 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/PlaceViewModel.kt @@ -5,19 +5,21 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.data.PlaceDBHelper import campus.tech.kakao.map.data.PlaceRepository import campus.tech.kakao.map.model.Place import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class PlaceViewModel(application: Application) : AndroidViewModel(application) { - private val placeRepository: PlaceRepository = PlaceRepository(application) - private val _searchResults = MutableLiveData>() + private val placeRepository: PlaceRepository + private val _searchResults = MutableLiveData>() val searchResults: LiveData> get() = _searchResults init { + val dbHelper = PlaceDBHelper(application) + placeRepository = PlaceRepository(dbHelper) clearSearchResults() } @@ -34,11 +36,9 @@ class PlaceViewModel(application: Application) : AndroidViewModel(application) { } fun searchPlacesByCategory(category: String) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - val results = placeRepository.getPlacesByCategory(category) - _searchResults.postValue(results) - } + viewModelScope.launch(Dispatchers.IO) { + val results = placeRepository.getPlacesByCategory(category) + _searchResults.postValue(results) } } } diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt index c65d4d10..85bd3c85 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedSearchWordViewModel.kt @@ -5,47 +5,42 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.data.SavedSearchWordDBHelper import campus.tech.kakao.map.data.SavedSearchWordRepository import campus.tech.kakao.map.model.SavedSearchWord import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class SavedSearchWordViewModel(application: Application) : AndroidViewModel(application) { - private val repository: SavedSearchWordRepository = SavedSearchWordRepository(application) + private val repository: SavedSearchWordRepository private val _savedSearchWords = MutableLiveData>() - val savedSearchWords: LiveData> get() = _savedSearchWords init { + val dbHelper = SavedSearchWordDBHelper(application) + repository = SavedSearchWordRepository(dbHelper) getAllSearchWords() } fun insertSearchWord(searchWord: SavedSearchWord) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - repository.insertOrUpdateSearchWord(searchWord) - getAllSearchWords() - } + viewModelScope.launch(Dispatchers.IO) { + repository.insertOrUpdateSearchWord(searchWord) + getAllSearchWords() } } fun deleteSearchWordById(searchWord: SavedSearchWord) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - repository.deleteSearchWordById(searchWord.id) - val currentList = _savedSearchWords.value ?: emptyList() - _savedSearchWords.postValue(currentList - searchWord) - } + viewModelScope.launch(Dispatchers.IO) { + repository.deleteSearchWordById(searchWord.id) + val currentList = _savedSearchWords.value ?: emptyList() + _savedSearchWords.postValue(currentList - searchWord) } } private fun getAllSearchWords() { - viewModelScope.launch { - withContext(Dispatchers.IO) { - val searchWords = repository.getAllSearchWords() - _savedSearchWords.postValue(searchWords) - } + viewModelScope.launch(Dispatchers.IO) { + val searchWords = repository.getAllSearchWords() + _savedSearchWords.postValue(searchWords) } } }