-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
부산대 Android_김정희_2주차 과제_2단계 #52
base: lovelhee
Are you sure you want to change the base?
Changes from all commits
1e95da9
4a7f9a8
42afbd1
517f77f
7ce9207
97252e2
de8235e
c09822b
6648952
f768d06
4667bef
34d7f19
af00889
4696833
270aa41
d8b123a
ba8c9e9
d87066d
14eab04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,37 @@ | ||
# android-map-keyword | ||
|
||
## Step1 | ||
|
||
### 📜 Description | ||
|
||
카카오맵 클론 코딩 _ 로컬 데이터 | ||
|
||
|
||
### 🎯 Tasks | ||
|
||
- Main 기능 : **검색 기능** | ||
|
||
- 검색어 입력 및 검색 결과를 표시할 기본 레이아웃을 구현 | ||
- 검색에 사용될 데이터를 로컬 데이터베이스에 생성 | ||
|
||
--- | ||
|
||
## Step2 | ||
|
||
### 📜 Description | ||
|
||
카카오맵 클론 코딩 _ 검색 및 삭제 | ||
|
||
|
||
### 🎯 Tasks | ||
|
||
- Main 기능 : **검색 및 삭제 기능** | ||
|
||
- 검색어를 입력하면 검색 결과 목록 표시 | ||
- 검색 결과 목록은 세로 스크롤이 됨 | ||
- 입력한 검색어는 X를 눌러서 삭제 | ||
- 검색 결과 목록에서 하나의 항목 선택 | ||
- 선택된 항목은 검색어 저장 목록에 추가 | ||
- 저장된 검색어 목록은 가로 스크롤 | ||
- 저장된 검색어는 X를 눌러서 삭제 | ||
- 저장된 검색어는 앱을 재실행하여도 유지 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MapItem> { | ||
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<MapItem>() | ||
|
||
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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<KeywordAdapter.ViewHolder>() { | ||
|
||
interface OnKeywordRemoveListener { | ||
fun onKeywordRemove(keyword: String) | ||
} | ||
|
||
private val keywords = mutableListOf<String>() | ||
|
||
val currentKeywords: List<String> | ||
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<String>) { | ||
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) | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
Comment on lines
+48
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
지금은 앞서가는 내용이지만, UI 클릭/이벤트를 다룰때 지금은 검색어를 입력할때마다 DB를 조회하게 되는데요. 또 다르게는 쓰레드 / 코루틴 같이 비동기로 처리할 수 있는 방법들을 사용하는것도 도움이 될것 같습니다. |
||
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() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<List<MapItem>>() | ||
val searchResults: LiveData<List<MapItem>> get() = _searchResults | ||
|
||
fun insertData() { | ||
dbHelper.insertData() | ||
} | ||
|
||
fun searchPlaces(keyword: String) { | ||
val results = dbHelper.searchPlaces(keyword) | ||
_searchResults.postValue(results) | ||
} | ||
Comment on lines
+19
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
ViewModel 에서 조회한 데이터를 -> Activity로 넘기는 처리가 필요한데 다만, |
||
} | ||
|
||
data class MapItem( | ||
val name: String, | ||
val address: String, | ||
val category: String | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 쿼리로 데이터 존재여부를 알수있다면 괜찮습니다.
SELECT EXISTS ...
문법도 있었던것 같네요. 차이점이 있는지 조사해보시고, EXISTS를 명시적으로 사용하는것도 괜찮을것 같습니다.