diff --git a/README.md b/README.md index df2a6477..d39b9734 100644 --- a/README.md +++ b/README.md @@ -1 +1,37 @@ # android-map-keyword + +## Step1 + +### ๐Ÿ“œ Description + +์นด์นด์˜ค๋งต ํด๋ก  ์ฝ”๋”ฉ _ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ + + +### ๐ŸŽฏ Tasks + +- Main ๊ธฐ๋Šฅ : **๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ** + +- ๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ ๋ฐ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•  ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌํ˜„ +- ๊ฒ€์ƒ‰์— ์‚ฌ์šฉ๋  ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒ์„ฑ + +--- + +## Step2 + +### ๐Ÿ“œ Description + +์นด์นด์˜ค๋งต ํด๋ก  ์ฝ”๋”ฉ _ ๊ฒ€์ƒ‰ ๋ฐ ์‚ญ์ œ + + +### ๐ŸŽฏ Tasks + +- Main ๊ธฐ๋Šฅ : **๊ฒ€์ƒ‰ ๋ฐ ์‚ญ์ œ ๊ธฐ๋Šฅ** + +- ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ชฉ๋ก ํ‘œ์‹œ + - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ชฉ๋ก์€ ์„ธ๋กœ ์Šคํฌ๋กค์ด ๋จ +- ์ž…๋ ฅํ•œ ๊ฒ€์ƒ‰์–ด๋Š” X๋ฅผ ๋ˆŒ๋Ÿฌ์„œ ์‚ญ์ œ +- ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ชฉ๋ก์—์„œ ํ•˜๋‚˜์˜ ํ•ญ๋ชฉ ์„ ํƒ + - ์„ ํƒ๋œ ํ•ญ๋ชฉ์€ ๊ฒ€์ƒ‰์–ด ์ €์žฅ ๋ชฉ๋ก์— ์ถ”๊ฐ€ + - ์ €์žฅ๋œ ๊ฒ€์ƒ‰์–ด ๋ชฉ๋ก์€ ๊ฐ€๋กœ ์Šคํฌ๋กค + - ์ €์žฅ๋œ ๊ฒ€์ƒ‰์–ด๋Š” X๋ฅผ ๋ˆŒ๋Ÿฌ์„œ ์‚ญ์ œ + - ์ €์žฅ๋œ ๊ฒ€์ƒ‰์–ด๋Š” ์•ฑ์„ ์žฌ์‹คํ–‰ํ•˜์—ฌ๋„ ์œ ์ง€ \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/DbHelper.kt b/app/src/main/java/campus/tech/kakao/map/DbHelper.kt new file mode 100644 index 00000000..1f28e9e6 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/DbHelper.kt @@ -0,0 +1,118 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class DbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + + companion object { + private const val DATABASE_NAME = "location.db" + private const val DATABASE_VERSION = 1 + } + + override fun onCreate(db: SQLiteDatabase) { + + val createCafeTable = """ + CREATE TABLE ${MapContract.TABLE_CAFE} ( + ${MapContract.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${MapContract.COLUMN_NAME} TEXT, + ${MapContract.COLUMN_ADDRESS} TEXT, + ${MapContract.COLUMN_CATEGORY} TEXT + ) + """.trimIndent() + + val createPharmacyTable = """ + CREATE TABLE ${MapContract.TABLE_PHARMACY} ( + ${MapContract.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${MapContract.COLUMN_NAME} TEXT, + ${MapContract.COLUMN_ADDRESS} TEXT, + ${MapContract.COLUMN_CATEGORY} TEXT + ) + """.trimIndent() + + db.execSQL(createCafeTable) + db.execSQL(createPharmacyTable) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("DROP TABLE IF EXISTS ${MapContract.TABLE_CAFE}") + db.execSQL("DROP TABLE IF EXISTS ${MapContract.TABLE_PHARMACY}") + onCreate(db) + } + + // step1 ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ • ๋ถ€๋ถ„ + fun insertData() { + val db = writableDatabase + + for (i in 1..9) { + val name = "์นดํŽ˜$i" + // ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ์ฒดํฌ ํ›„ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์ฒ˜๋ฆฌ + if (!dataExists(MapContract.TABLE_CAFE, name)) { + val insertCafe = """ + INSERT INTO ${MapContract.TABLE_CAFE} (${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY}) + VALUES ('$name', '์„œ์šธ ์„ฑ๋™๊ตฌ ์„ฑ์ˆ˜๋™ $i', '์นดํŽ˜') + """.trimIndent() + db.execSQL(insertCafe) + } + } + + for (i in 1..9) { + val name = "์•ฝ๊ตญ$i" + if (!dataExists(MapContract.TABLE_PHARMACY, name)) { + val insertPharmacy = """ + INSERT INTO ${MapContract.TABLE_PHARMACY} (${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY}) + VALUES ('$name', '์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋Œ€์น˜๋™ $i', '์•ฝ๊ตญ') + """.trimIndent() + db.execSQL(insertPharmacy) + } + } + + db.close() + } + + // ๋ฐ์ดํ„ฐ ์กด์žฌ ์—ฌ๋ถ€ ์ฒดํ‚น ๋ฉ”์†Œ๋“œ + private fun dataExists(tableName: String, name: String): Boolean { + val db = readableDatabase + val query = "SELECT 1 FROM $tableName WHERE ${MapContract.COLUMN_NAME} = ?" + val cursor = db.rawQuery(query, arrayOf(name)) + val exists = cursor.moveToFirst() + cursor.close() + return exists + } + + fun searchPlaces(keyword: String): List { + val db = readableDatabase + val query = """ + SELECT ${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY} + FROM ${MapContract.TABLE_CAFE} + WHERE ${MapContract.COLUMN_NAME} LIKE ? + UNION + SELECT ${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY} + FROM ${MapContract.TABLE_PHARMACY} + WHERE ${MapContract.COLUMN_NAME} LIKE ? + """.trimIndent() + val cursor = db.rawQuery(query, arrayOf("%$keyword%", "%$keyword%")) + val results = mutableListOf() + + cursor.use { + val nameIndex = cursor.getColumnIndex(MapContract.COLUMN_NAME) + val addressIndex = cursor.getColumnIndex(MapContract.COLUMN_ADDRESS) + val categoryIndex = cursor.getColumnIndex(MapContract.COLUMN_CATEGORY) + + if (nameIndex != -1 && addressIndex != -1 && categoryIndex != -1) { + if (cursor.moveToFirst()) { + do { + val name = cursor.getString(nameIndex) + val address = cursor.getString(addressIndex) + val category = cursor.getString(categoryIndex) + results.add(MapItem(name, address, category)) + } while (cursor.moveToNext()) + } + } + } + + return results + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/KeywordAdapter.kt b/app/src/main/java/campus/tech/kakao/map/KeywordAdapter.kt new file mode 100644 index 00000000..6b3be174 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/KeywordAdapter.kt @@ -0,0 +1,66 @@ +package campus.tech.kakao.map + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView + +class KeywordAdapter(private val listener: OnKeywordRemoveListener) : RecyclerView.Adapter() { + + interface OnKeywordRemoveListener { + fun onKeywordRemove(keyword: String) + } + + private val keywords = mutableListOf() + + val currentKeywords: List + get() = keywords + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_keyword, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val keyword = keywords[position] + holder.bind(keyword) + } + + override fun getItemCount(): Int { + return keywords.size + } + + fun submitList(newKeywords: List) { + val oldSize = keywords.size + keywords.clear() + notifyItemRangeRemoved(0, oldSize) + keywords.addAll(newKeywords) + notifyItemRangeInserted(0, newKeywords.size) + } + + fun addKeyword(keyword: String) { + if (!keywords.contains(keyword)) { + keywords.add(keyword) + notifyItemInserted(keywords.size - 1) + } + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val tvKeyword: TextView = itemView.findViewById(R.id.tvKeyword) + private val ivRemove: ImageView = itemView.findViewById(R.id.imageView) + + fun bind(keyword: String) { + tvKeyword.text = keyword + ivRemove.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + keywords.removeAt(position) + notifyItemRemoved(position) + listener.onKeywordRemove(keyword) + } + } + } + } +} 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..7593287f 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -1,11 +1,100 @@ package campus.tech.kakao.map import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class MainActivity : AppCompatActivity(), SearchResultAdapter.OnItemClickListener, KeywordAdapter.OnKeywordRemoveListener { + + private lateinit var mapViewModel: MapViewModel + private lateinit var etKeywords: EditText + private lateinit var rvSearchResult: RecyclerView + private lateinit var rvKeywords: RecyclerView + private lateinit var tvNoResults: TextView + private lateinit var ivClear: ImageView + + private val searchResultAdapter = SearchResultAdapter(this) + private val keywordAdapter = KeywordAdapter(this) -class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + etKeywords = findViewById(R.id.etKeywords) + rvSearchResult = findViewById(R.id.rvSearchResult) + rvKeywords = findViewById(R.id.rvKeywords) + tvNoResults = findViewById(R.id.tvNoResults) + ivClear = findViewById(R.id.ivClear) + + rvSearchResult.layoutManager = LinearLayoutManager(this) + rvSearchResult.adapter = searchResultAdapter + + rvKeywords.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + rvKeywords.adapter = keywordAdapter + + mapViewModel = ViewModelProvider(this).get(MapViewModel::class.java) + mapViewModel.insertData() + + etKeywords.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) { + val keyword = s.toString() + if (keyword.isNotEmpty()) { + mapViewModel.searchPlaces(keyword) + } else { + searchResultAdapter.submitList(emptyList()) + tvNoResults.visibility = TextView.VISIBLE + rvSearchResult.visibility = RecyclerView.GONE + } + } + override fun afterTextChanged(s: Editable?) {} + }) + + mapViewModel.searchResults.observe(this, Observer { results -> + if (results.isEmpty()) { + tvNoResults.visibility = TextView.VISIBLE + rvSearchResult.visibility = RecyclerView.GONE + } else { + tvNoResults.visibility = TextView.GONE + rvSearchResult.visibility = RecyclerView.VISIBLE + searchResultAdapter.submitList(results) + } + }) + + ivClear.setOnClickListener { + etKeywords.text.clear() + } + + loadKeywords() + } + + override fun onItemClick(item: MapItem) { + keywordAdapter.addKeyword(item.name) + saveKeywords() + } + + override fun onKeywordRemove(keyword: String) { + saveKeywords() + } + + private fun loadKeywords() { + val sharedPreferences = getSharedPreferences("keywords", MODE_PRIVATE) + val keywords = sharedPreferences.getStringSet("keywords", setOf())?.toMutableList() ?: mutableListOf() + keywordAdapter.submitList(keywords) + } + + private fun saveKeywords() { + val sharedPreferences = getSharedPreferences("keywords", MODE_PRIVATE) + val editor = sharedPreferences.edit() + editor.putStringSet("keywords", keywordAdapter.currentKeywords.toSet()) + editor.apply() } } diff --git a/app/src/main/java/campus/tech/kakao/map/MapContract.kt b/app/src/main/java/campus/tech/kakao/map/MapContract.kt new file mode 100644 index 00000000..310e464a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapContract.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map + +object MapContract +{ + const val TABLE_CAFE = "Cafe" + const val TABLE_PHARMACY = "Pharmacy" + + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_ADDRESS = "address" + const val COLUMN_CATEGORY = "category" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/MapViewModel.kt new file mode 100644 index 00000000..266315cc --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapViewModel.kt @@ -0,0 +1,29 @@ +package campus.tech.kakao.map + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +class MapViewModel(application: Application) : AndroidViewModel(application) { + + private val dbHelper = DbHelper(application) + + private val _searchResults = MutableLiveData>() + val searchResults: LiveData> get() = _searchResults + + fun insertData() { + dbHelper.insertData() + } + + fun searchPlaces(keyword: String) { + val results = dbHelper.searchPlaces(keyword) + _searchResults.postValue(results) + } +} + +data class MapItem( + val name: String, + val address: String, + val category: String +) diff --git a/app/src/main/java/campus/tech/kakao/map/SearchResultAdapter.kt b/app/src/main/java/campus/tech/kakao/map/SearchResultAdapter.kt new file mode 100644 index 00000000..a768ee16 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchResultAdapter.kt @@ -0,0 +1,57 @@ +package campus.tech.kakao.map + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView + +class SearchResultAdapter(private val listener: OnItemClickListener) : ListAdapter(DiffCallback()) { + + interface OnItemClickListener { + fun onItemClick(item: MapItem) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_search_result, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item) + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val tvPlaceName: TextView = itemView.findViewById(R.id.tvPlaceName) + private val tvPlaceAddress: TextView = itemView.findViewById(R.id.tvPlaceAddress) + private val tvCategory: TextView = itemView.findViewById(R.id.tvCategory) + + init { + itemView.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onItemClick(getItem(position)) + } + } + } + + fun bind(item: MapItem) { + tvPlaceName.text = item.name + tvPlaceAddress.text = item.address + tvCategory.text = item.category + } + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MapItem, newItem: MapItem): Boolean { + return oldItem.name == newItem.name && oldItem.address == newItem.address + } + + override fun areContentsTheSame(oldItem: MapItem, newItem: MapItem): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/res/drawable/close_24px.xml b/app/src/main/res/drawable/close_24px.xml new file mode 100644 index 00000000..7a0ff35d --- /dev/null +++ b/app/src/main/res/drawable/close_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/location_on_24px.xml b/app/src/main/res/drawable/location_on_24px.xml new file mode 100644 index 00000000..871f7cee --- /dev/null +++ b/app/src/main/res/drawable/location_on_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..06df549b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,15 +5,66 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".MainActivity" + android:padding="16dp"> + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/rvKeywords" + android:scrollbars="vertical"/> diff --git a/app/src/main/res/layout/item_keyword.xml b/app/src/main/res/layout/item_keyword.xml new file mode 100644 index 00000000..a8c3bac3 --- /dev/null +++ b/app/src/main/res/layout/item_keyword.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml new file mode 100644 index 00000000..fb343dda --- /dev/null +++ b/app/src/main/res/layout/item_search_result.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + \ 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..6f176e24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,12 @@ Map + ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”. + ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + ๊ฒ€์ƒ‰์–ด ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ + keywordCancle + keyword + locationMarker + place name + place address + category \ No newline at end of file