diff --git a/README.md b/README.md index df2a6477..04fe1262 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ # android-map-keyword + +### Week2-Step1 로컬 데이터 +- 검색어 입력 및 검색 결과를 표시할 기본 레이아웃을 구현한다. +- 검색에 사용될 데이터를 로컬 데이터베이스에 생성한다. + - 검색 데이터의 저장은 SQLite를 사용한다. +- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다. + +### Week2-Step2 검색 +- 검색어를 입력하면 검색 결과 목록이 표시된다. +- 검색 결과 목록은 세로 스크롤이 된다. +- 입력한 검색어는 X를 눌러서 삭제할 수 있다. +- 검색 결과 목록에서 하나의 항목을 선택할 수 있다. +- 선택된 항목은 검색어 저장 목록에 추가된다. +- 저장된 검색어 목록은 가로 스크롤이 된다. +- 저장된 검색어는 X를 눌러서 삭제할 수 있다. +- 저장된 검색어는 앱을 재실행하여도 유지된다. \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..156db66d 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/model/Place.kt b/app/src/main/java/campus/tech/kakao/map/model/Place.kt new file mode 100644 index 00000000..98c4fcf1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/Place.kt @@ -0,0 +1,8 @@ +package campus.tech.kakao.map.model + +data class Place( + val name: String, + val address: String, + val category: String +) + diff --git a/app/src/main/java/campus/tech/kakao/map/model/PlaceContract.kt b/app/src/main/java/campus/tech/kakao/map/model/PlaceContract.kt new file mode 100644 index 00000000..265bf150 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/PlaceContract.kt @@ -0,0 +1,29 @@ +package campus.tech.kakao.map.model + +import android.provider.BaseColumns + +object PlaceContract { + object PlaceEntry : BaseColumns { + const val TABLE_NAME = "place" + const val COLUMN_PLACE_NAME = "place" + const val COLUMN_PLACE_ADDRESS = "address" + const val COLUMN_PLACE_CATEGORY = "category" + const val CREATE_QUERY = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" + + "${BaseColumns._ID} INTEGER PRIMARY KEY," + + "$COLUMN_PLACE_NAME varchar(30)," + + "$COLUMN_PLACE_ADDRESS varchar(255)," + + "$COLUMN_PLACE_CATEGORY varchar(30))" + const val DROP_QUERY = "DROP TABLE IF EXISTS $TABLE_NAME" + } + + object SavePlaceEntry : BaseColumns { + const val TABLE_NAME = "savePlace" + const val COLUMN_PLACE_NAME = "savePlaceName" + private const val COLUMN_TIMESTAMP = "timestamp" + const val CREATE_QUERY = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" + + "${BaseColumns._ID} INTEGER PRIMARY KEY," + + "$COLUMN_PLACE_NAME varchar(30)," + + "$COLUMN_TIMESTAMP DATETIME DEFAULT CURRENT_TIMESTAMP)" + const val DROP_QUERY = "DROP TABLE IF EXISTS $TABLE_NAME" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/PlaceDBHelper.kt b/app/src/main/java/campus/tech/kakao/map/model/PlaceDBHelper.kt new file mode 100644 index 00000000..2840f2bd --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/PlaceDBHelper.kt @@ -0,0 +1,180 @@ +package campus.tech.kakao.map.model + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log + +class PlaceDBHelper(context: Context) : SQLiteOpenHelper(context, "placedb", null, 1) { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL(PlaceContract.PlaceEntry.CREATE_QUERY) + db?.execSQL(PlaceContract.SavePlaceEntry.CREATE_QUERY) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL(PlaceContract.PlaceEntry.DROP_QUERY) + db?.execSQL(PlaceContract.SavePlaceEntry.DROP_QUERY) + onCreate(db) + } + + private fun isDataExists(category: String): Boolean { + val db: SQLiteDatabase = this.readableDatabase + val cursor: Cursor = db.query( + PlaceContract.PlaceEntry.TABLE_NAME, + arrayOf(PlaceContract.PlaceEntry.COLUMN_PLACE_CATEGORY), + "${PlaceContract.PlaceEntry.COLUMN_PLACE_CATEGORY} = ?", + arrayOf(category), + null, + null, + null + ) + val exists = cursor.count > 0 + cursor.close() + db.close() + return exists + } + + fun insertPlaceDummyData(name: String, address: String, category: String) { + if (isDataExists(category)) { + Log.d("ddangcong80", "Category $category already exists.") + return + } + + val db: SQLiteDatabase = this.writableDatabase + val values = ContentValues() + for (i in 1..15) { + values.put(PlaceContract.PlaceEntry.COLUMN_PLACE_NAME, name + i) + values.put(PlaceContract.PlaceEntry.COLUMN_PLACE_ADDRESS, address + i) + values.put(PlaceContract.PlaceEntry.COLUMN_PLACE_CATEGORY, category) + db.insert(PlaceContract.PlaceEntry.TABLE_NAME, null, values) + } + db.close() + } + + fun getSearchPlaces(placeCategory: String): MutableList { + val db: SQLiteDatabase = this.readableDatabase + val places = mutableListOf() + var cursor: Cursor? = null + try { + val selection = "${PlaceContract.PlaceEntry.COLUMN_PLACE_CATEGORY} LIKE ?" + val selectionArgs = arrayOf("$placeCategory%") + + cursor = db.query( + PlaceContract.PlaceEntry.TABLE_NAME, + null, + selection, + selectionArgs, + null, + null, + null + ) + + if (cursor != null) { + while (cursor.moveToNext()) { + val name = + cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_PLACE_NAME)) + val address = + cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_PLACE_ADDRESS)) + val category = + cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_PLACE_CATEGORY)) + + places.add(Place(name, address, category)) + } + } + } catch (e: Exception) { + Log.e("ddangcong80", "Error", e) + } finally { + cursor?.close() + db.close() + } + + return places + } + + + fun savePlaces(placeName: String) { + val db: SQLiteDatabase = this.writableDatabase + val values = ContentValues() + values.put(PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME, placeName) + + val cursor = db.query( + PlaceContract.SavePlaceEntry.TABLE_NAME, + arrayOf(PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME), + "${PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME} = ?", + arrayOf(placeName), + null, + null, + null + ) + + if (cursor.moveToFirst()) { + db.delete( + PlaceContract.SavePlaceEntry.TABLE_NAME, + "${PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME} = ?", + arrayOf(placeName) + ) + } + cursor.close() + db.insert(PlaceContract.SavePlaceEntry.TABLE_NAME, null, values) + db.close() + } + + fun showSavePlace(): MutableList { + val db: SQLiteDatabase = this.readableDatabase + val savePlaces = mutableListOf() + var cursor: Cursor? = null + try { + cursor = db.query( + PlaceContract.SavePlaceEntry.TABLE_NAME, + null, + null, + null, + null, + null, + null + ) + + if (cursor != null) { + while (cursor.moveToNext()) { + val name = + cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME)) + + savePlaces.add(SavePlace(name)) + } + } + } catch (e: Exception) { + Log.e("ddangcong80", "Error", e) + } finally { + cursor?.close() + db.close() + } + + return savePlaces + } + + fun deleteSavedPlace(savedPlaceName: String) { + val db: SQLiteDatabase = this.writableDatabase + + val cursor = db.query( + PlaceContract.SavePlaceEntry.TABLE_NAME, + arrayOf(PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME), + "${PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME} = ?", + arrayOf(savedPlaceName), + null, + null, + null + ) + + if (cursor.moveToFirst()) { + db.delete( + PlaceContract.SavePlaceEntry.TABLE_NAME, + "${PlaceContract.SavePlaceEntry.COLUMN_PLACE_NAME} = ?", + arrayOf(savedPlaceName) + ) + } + cursor.close() + db.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/SavePlace.kt b/app/src/main/java/campus/tech/kakao/map/model/SavePlace.kt new file mode 100644 index 00000000..79d24783 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/SavePlace.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.model + +data class SavePlace( + val savePlace: String, +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/SearchRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SearchRepository.kt new file mode 100644 index 00000000..79378498 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/SearchRepository.kt @@ -0,0 +1,45 @@ +package campus.tech.kakao.map.repository + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.util.Log +import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.model.PlaceContract +import campus.tech.kakao.map.model.PlaceDBHelper +import campus.tech.kakao.map.model.SavePlace + +class SearchRepository(context: Context) { + private val dbHelper = PlaceDBHelper(context) + + fun insertPlaceDummyData(name: String, address: String, category: String) { + dbHelper.insertPlaceDummyData(name, address, category) + } + + fun getSearchPlaces(placeCategory: String): MutableList { + return dbHelper.getSearchPlaces(placeCategory) + } + + fun savePlaces(placeName: String) { + dbHelper.savePlaces(placeName) + } + + fun showSavePlace(): MutableList { + return dbHelper.showSavePlace() + } + + fun deleteSavedPlace(savedPlaceName: String) { + dbHelper.deleteSavedPlace(savedPlaceName) + } + + fun savePlacesAndUpdate(placeName: String): MutableList { + savePlaces(placeName) + return showSavePlace() + } + + fun deleteSavedPlacesAndUpdate(savedPlaceName: String): MutableList { + deleteSavedPlace(savedPlaceName) + return showSavePlace() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/SavePlaceAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/SavePlaceAdapter.kt new file mode 100644 index 00000000..eb311ef4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SavePlaceAdapter.kt @@ -0,0 +1,53 @@ +package campus.tech.kakao.map.view + +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 +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.SavePlace + +class SavePlaceAdapter( + private var savePlaces: List, + private val onItemClickListener: (SavePlace) -> Unit +) : RecyclerView.Adapter() { + class SavePlaceViewHolder( + itemView: View, + private val onItemClickListener: (SavePlace) -> Unit + ) : RecyclerView.ViewHolder(itemView) { + private val savePlaceTextView: TextView = itemView.findViewById(R.id.savePlace) + private val savePlaceDeleteBtn: ImageView = itemView.findViewById(R.id.saveCancelBtn) + + fun bind(savePlace: SavePlace) { + savePlaceTextView.text = savePlace.savePlace + savePlaceDeleteBtn.setOnClickListener { + onItemClickListener(savePlace) + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SavePlaceViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.saveplace_item, parent, false) + return SavePlaceViewHolder(view, onItemClickListener) + } + + override fun onBindViewHolder(holder: SavePlaceViewHolder, position: Int) { + val savePlace = savePlaces[position] + holder.bind(savePlace) + } + + override fun getItemCount(): Int { + return savePlaces.size + } + + fun updateData(newSavePlaces: List) { + savePlaces = newSavePlaces + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt new file mode 100644 index 00000000..277a1ec1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt @@ -0,0 +1,109 @@ +package campus.tech.kakao.map.view + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.viewmodel.SearchViewModel + +class SearchActivity : AppCompatActivity() { + private lateinit var viewModel: SearchViewModel + private lateinit var editText: EditText + private lateinit var cancelBtn: ImageView + private lateinit var recyclerView: RecyclerView + private lateinit var noSearchLayout: LinearLayout + private lateinit var searchAdapter: SearchAdapter + private lateinit var saveRecyclerView: RecyclerView + private lateinit var savePlaceAdapter: SavePlaceAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_search) + + viewModel = ViewModelProvider( + this, + ViewModelProvider.AndroidViewModelFactory.getInstance(application) + )[SearchViewModel::class.java] + + initView() + setListeners() + setAdapters() + observeViewModel() + } + + private fun initView() { + editText = findViewById(R.id.searchText) + cancelBtn = findViewById(R.id.cancelBtn) + recyclerView = findViewById(R.id.searchPlaceView) + noSearchLayout = findViewById(R.id.noSearch) + saveRecyclerView = findViewById(R.id.savePlaceView) + + recyclerView.layoutManager = LinearLayoutManager(this) + saveRecyclerView.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + } + + private fun setListeners() { + cancelBtn.setOnClickListener { + editText.setText("") + updateViewVisibility(false) + } + + editText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s.isNullOrEmpty()) { + updateViewVisibility(false) + } else { + viewModel.searchPlaces(s.toString()) + } + } + + override fun afterTextChanged(s: Editable?) {} + }) + } + + private fun setAdapters() { + searchAdapter = SearchAdapter(emptyList()) { + viewModel.savePlaces(it.name) + } + recyclerView.adapter = searchAdapter + + savePlaceAdapter = SavePlaceAdapter(emptyList()) { + viewModel.deleteSavedPlace(it.savePlace) + } + saveRecyclerView.adapter = savePlaceAdapter + } + + private fun observeViewModel() { + viewModel.places.observe(this) { places -> + searchAdapter.updateData(places) + updateViewVisibility(places.isNotEmpty()) + } + + viewModel.savePlaces.observe(this) { savePlaces -> + savePlaceAdapter.updateData(savePlaces) + } + } + + private fun updateViewVisibility(hasPlaces: Boolean) { + if (hasPlaces) { + recyclerView.visibility = View.VISIBLE + noSearchLayout.visibility = View.GONE + } else { + recyclerView.visibility = View.GONE + noSearchLayout.visibility = View.VISIBLE + } + } + + +} diff --git a/app/src/main/java/campus/tech/kakao/map/view/SearchAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/SearchAdapter.kt new file mode 100644 index 00000000..9fecb1a4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SearchAdapter.kt @@ -0,0 +1,57 @@ +package campus.tech.kakao.map.view + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.Place + +class SearchAdapter( + private var places: List, + private val onItemClickListener: (Place) -> Unit +) : + RecyclerView.Adapter() { + + class SearchViewHolder( + itemView: View, + private val onItemClickListener: (Place) -> Unit + ) : RecyclerView.ViewHolder(itemView) { + private val nameTextView: TextView = itemView.findViewById(R.id.placeName) + private val addressTextView: TextView = itemView.findViewById(R.id.placeAddress) + private val categoryTextView: TextView = itemView.findViewById(R.id.placeCategory) + + fun bind(place: Place) { + nameTextView.text = place.name + addressTextView.text = place.address + categoryTextView.text = place.category + itemView.setOnClickListener { + onItemClickListener(place) + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SearchViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.place_item, parent, false) + return SearchViewHolder(view, onItemClickListener) + } + + override fun onBindViewHolder(holder: SearchViewHolder, position: Int) { + val place = places[position] + holder.bind(place) + } + + override fun getItemCount(): Int { + return places.size + } + + fun updateData(newPlaces: List) { + places = newPlaces + notifyDataSetChanged() + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchViewModel.kt new file mode 100644 index 00000000..17b1c462 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchViewModel.kt @@ -0,0 +1,41 @@ +package campus.tech.kakao.map.viewmodel + + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.model.SavePlace +import campus.tech.kakao.map.repository.SearchRepository + +class SearchViewModel(application: Application) : AndroidViewModel(application) { + private val searchRepo: SearchRepository = SearchRepository(application) + private val _places: MutableLiveData> = MutableLiveData() + private val _savePlaces: MutableLiveData> = MutableLiveData() + + val places: LiveData> get() = _places + val savePlaces: LiveData> get() = _savePlaces + + init { + insertDummyData("카페", "대전 유성구 궁동", "카페") + insertDummyData("약국", "대전 유성구 봉명동", "약국") + _savePlaces.value = searchRepo.showSavePlace() + } + + private fun insertDummyData(name: String, address: String, category: String) { + searchRepo.insertPlaceDummyData(name, address, category) + } + + fun searchPlaces(placeCategory: String) { + _places.value = searchRepo.getSearchPlaces(placeCategory) + } + + fun savePlaces(placeName: String) { + _savePlaces.value = searchRepo.savePlacesAndUpdate(placeName) + } + + fun deleteSavedPlace(savedPlaceName: String) { + _savePlaces.value = searchRepo.deleteSavedPlacesAndUpdate(savedPlaceName) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/cancel.png b/app/src/main/res/drawable/cancel.png new file mode 100644 index 00000000..da5858ec Binary files /dev/null and b/app/src/main/res/drawable/cancel.png differ diff --git a/app/src/main/res/drawable/cancel_btn.xml b/app/src/main/res/drawable/cancel_btn.xml new file mode 100644 index 00000000..1352cdd9 --- /dev/null +++ b/app/src/main/res/drawable/cancel_btn.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/place_icon.png b/app/src/main/res/drawable/place_icon.png new file mode 100644 index 00000000..5450933c Binary files /dev/null and b/app/src/main/res/drawable/place_icon.png differ 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..5316c68d --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/place_item.xml b/app/src/main/res/layout/place_item.xml new file mode 100644 index 00000000..8d2bd526 --- /dev/null +++ b/app/src/main/res/layout/place_item.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/saveplace_item.xml b/app/src/main/res/layout/saveplace_item.xml new file mode 100644 index 00000000..3de9719d --- /dev/null +++ b/app/src/main/res/layout/saveplace_item.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..61af2df4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,11 @@ Map + 검색어를 입력해 주세요. + cancelBtn + 검색 결과가 없습니다. + 이름 + 장소 + 카테고리 + 장소 + 검색기록삭제 \ No newline at end of file