From d8043f6b26f5159295ea3b9e28aa0c75a5380921 Mon Sep 17 00:00:00 2001 From: kimseongyu <50648835+kimseongyu@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:24:24 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B6=A9=EB=82=A8=EB=8C=80=20Android=5F?= =?UTF-8?q?=EA=B9=80=EC=84=A0=EA=B7=9C=204=EC=A3=BC=EC=B0=A8=20Step0=20(#1?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit * Merge : android-map-keyword into android-map-search (#8) * 충남대 Android_김선규 3주차 과제 Step1 (#47) * docs: add step1 requirements * chore: set for using android api * style: rename id in layout * feat: remove storeInfo for using api * feat: add connecting api for searching * style: rename variable name proper * 충남대 Android_김선규 3주차 과제 Step2 (#85) * style: function rename and split * feat: Change function to fit coroutine * docs: add step2 requirements * style: move from main to sub file * chore: set it up to work in the right environment * feat: display kakao map, when app is started --------- Co-authored-by: MyStoryG --- app/src/main/AndroidManifest.xml | 3 + .../tech/kakao/map/KakaoSearchKeywordAPI.kt | 13 +++ .../campus/tech/kakao/map/MainActivity.kt | 53 ++++++++- .../kakao/map/SavedSearchKeywordRepository.kt | 57 ++++++++++ .../kakao/map/SavedSearchKeywordsAdapter.kt | 40 +++++++ .../campus/tech/kakao/map/SearchKeyword.kt | 12 +++ .../campus/tech/kakao/map/SearchRepository.kt | 29 +++++ .../campus/tech/kakao/map/SearchResults.kt | 14 +++ .../tech/kakao/map/SearchResultsAdapter.kt | 45 ++++++++ .../campus/tech/kakao/map/SearchViewModel.kt | 42 ++++++++ .../tech/kakao/map/SearchViewModelFactory.kt | 14 +++ .../tech/kakao/map/SearchWindowActivity.kt | 101 ++++++++++++++++++ .../tech/kakao/map/StoreInfoDBHelper.kt | 30 ++++++ .../main/res/drawable/magnifying_glass.xml | 12 +++ 14 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/campus/tech/kakao/map/KakaoSearchKeywordAPI.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordsAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchKeyword.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchResults.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchResultsAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchViewModelFactory.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchWindowActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/StoreInfoDBHelper.kt create mode 100644 app/src/main/res/drawable/magnifying_glass.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e99f8409..1ae2d108 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,9 @@ android:supportsRtl="true" android:theme="@style/Theme.Map" tools:targetApi="31"> + diff --git a/app/src/main/java/campus/tech/kakao/map/KakaoSearchKeywordAPI.kt b/app/src/main/java/campus/tech/kakao/map/KakaoSearchKeywordAPI.kt new file mode 100644 index 00000000..7eff1e3e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/KakaoSearchKeywordAPI.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map + +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface KakaoSearchKeywordAPI { + @GET("/v2/local/search/keyword.json") + suspend fun getSearchKeyWord( + @Header("Authorization") key: String, + @Query("query") keyword: String + ): SearchResults +} \ No newline at end of file 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..1be01402 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,62 @@ package campus.tech.kakao.map +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.databinding.ActivityMainBinding +import com.kakao.vectormap.KakaoMap +import com.kakao.vectormap.KakaoMapReadyCallback +import com.kakao.vectormap.KakaoMapSdk +import com.kakao.vectormap.MapLifeCycleCallback +import com.kakao.vectormap.MapView +import java.lang.Exception class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + lateinit var mapView: MapView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + gotoSearchWindowBtnListener() + displayKakaoMap() + } + + override fun onResume() { + super.onResume() + mapView.resume() + } + + override fun onPause() { + super.onPause() + mapView.pause() + } + + fun gotoSearchWindowBtnListener(){ + binding.gotoSearchWindow.setOnClickListener { + val intent = Intent(this, SearchWindowActivity::class.java) + startActivity(intent) + } + } + + fun displayKakaoMap(){ + KakaoMapSdk.init(this, BuildConfig.KAKAO_API_KEY) + mapView = binding.mapView + mapView.start(object: MapLifeCycleCallback() { + override fun onMapDestroy() { + + } + + override fun onMapError(p0: Exception?) { + + } + }, object: KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + + } + }) } } + diff --git a/app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordRepository.kt b/app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordRepository.kt new file mode 100644 index 00000000..7b856295 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordRepository.kt @@ -0,0 +1,57 @@ +package campus.tech.kakao.map + +import android.content.ContentValues +import android.content.Context + +class SavedSearchKeywordRepository(context: Context) { + + private val dbHelper = StoreInfoDBHelper(context) + + fun saveSearchKeyword(searchKeyword: SearchKeyword) { + val db = dbHelper.writableDatabase + val values = ContentValues().apply { + put(SearchKeywordEntry.SEARCH_KEYWORD, searchKeyword.searchKeyword) + } + db.insert(SearchKeywordEntry.TABLE_NAME, null, values) + db.close() + } + + fun getSavedSearchKeywords(): List { + val db = dbHelper.readableDatabase + + val projection = arrayOf( + SearchKeywordEntry.SEARCH_KEYWORD + ) + + val cursor = db.query( + SearchKeywordEntry.TABLE_NAME, + projection, + null, + null, + null, + null, + null + ) + + val savedSearchKeywords = mutableListOf() + with(cursor) { + while (moveToNext()) { + val searchWord = getString(getColumnIndexOrThrow(SearchKeywordEntry.SEARCH_KEYWORD)) + + val savedSearchWord = SearchKeyword(searchWord) + savedSearchKeywords.add(savedSearchWord) + } + } + cursor.close() + db.close() + return savedSearchKeywords + } + + fun delSavedSearchKeyword(searchKeyword: SearchKeyword) { + val db = dbHelper.writableDatabase + val selection = "${SearchKeywordEntry.SEARCH_KEYWORD} = ?" + val selectionArgs = arrayOf(searchKeyword.searchKeyword) + db.delete(SearchKeywordEntry.TABLE_NAME, selection, selectionArgs) + db.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordsAdapter.kt b/app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordsAdapter.kt new file mode 100644 index 00000000..aeb2ad4f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SavedSearchKeywordsAdapter.kt @@ -0,0 +1,40 @@ +package campus.tech.kakao.map + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.databinding.SavedSearchKeywordItemBinding + +class SavedSearchKeywordsAdapter( + private val savedSearchKeywords: List, + private val layoutInflater: LayoutInflater, + private val delSavedSearchKeyword: (SearchKeyword) -> Unit +) : + RecyclerView.Adapter() { + inner class ViewHolder(binding: SavedSearchKeywordItemBinding) : + RecyclerView.ViewHolder(binding.root) { + val savedSearchKeyword: TextView = binding.SavedSearchKeyword + + init { + binding.delSavedSearchKeyword.setOnClickListener { + val searchKeyword = SearchKeyword(savedSearchKeyword.text.toString()) + delSavedSearchKeyword(searchKeyword) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = SavedSearchKeywordItemBinding.inflate(layoutInflater, parent, false) + return ViewHolder(view) + } + + override fun getItemCount(): Int { + return savedSearchKeywords.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = savedSearchKeywords[position] + holder.savedSearchKeyword.text = item.searchKeyword + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchKeyword.kt b/app/src/main/java/campus/tech/kakao/map/SearchKeyword.kt new file mode 100644 index 00000000..b69d2e11 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchKeyword.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map + +import android.provider.BaseColumns + +data class SearchKeyword( + val searchKeyword: String +) + +object SearchKeywordEntry : BaseColumns { + const val TABLE_NAME = "search_keyword" + const val SEARCH_KEYWORD = "search_keyword" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchRepository.kt b/app/src/main/java/campus/tech/kakao/map/SearchRepository.kt new file mode 100644 index 00000000..47dc800f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchRepository.kt @@ -0,0 +1,29 @@ +package campus.tech.kakao.map + +import kotlinx.coroutines.coroutineScope +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class SearchRepository { + companion object { + const val BASE_URL = "https://dapi.kakao.com" + const val API_KEY = "KakaoAK ${BuildConfig.KAKAO_REST_API_KEY}" + } + + private val retrofitKakaoSearchKeyword = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(KakaoSearchKeywordAPI::class.java) + + suspend fun Search(searchKeyword: SearchKeyword): List { + if (searchKeyword.searchKeyword == "") + return emptyList() + + return coroutineScope { + retrofitKakaoSearchKeyword + .getSearchKeyWord(API_KEY, searchKeyword.searchKeyword) + .places + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchResults.kt b/app/src/main/java/campus/tech/kakao/map/SearchResults.kt new file mode 100644 index 00000000..de0ddba0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchResults.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map + +import com.google.gson.annotations.SerializedName + +data class SearchResults( + @SerializedName("documents") + val places: List +) + +data class Place( + val place_name: String, + val category_name: String, + val address_name: String +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchResultsAdapter.kt b/app/src/main/java/campus/tech/kakao/map/SearchResultsAdapter.kt new file mode 100644 index 00000000..f331831d --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchResultsAdapter.kt @@ -0,0 +1,45 @@ +package campus.tech.kakao.map + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.databinding.SearchResultItemBinding + + +class SearchResultsAdapter( + private val searchResults: List, + private val layoutInflater: LayoutInflater, + private val saveStoreName: (SearchKeyword) -> Unit +) : + RecyclerView.Adapter() { + inner class ViewHolder(binding: SearchResultItemBinding) : + RecyclerView.ViewHolder(binding.root) { + val placeName: TextView = binding.placeName + val addressName: TextView = binding.addressName + val categoryName: TextView = binding.categoryName + + init { + binding.root.setOnClickListener { + val searchKeyword = SearchKeyword(placeName.text.toString()) + saveStoreName(searchKeyword) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = SearchResultItemBinding.inflate(layoutInflater, parent, false) + return ViewHolder(view) + } + + override fun getItemCount(): Int { + return searchResults.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = searchResults[position] + holder.placeName.text = item.place_name + holder.addressName.text = item.address_name + holder.categoryName.text = item.category_name + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt new file mode 100644 index 00000000..d3484bb0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt @@ -0,0 +1,42 @@ +package campus.tech.kakao.map + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class SearchViewModel( + private val searchRepository: SearchRepository, + private val savedSearchKeywordRepository: SavedSearchKeywordRepository +) : ViewModel() { + + private val _searchResults = MutableStateFlow>(emptyList()) + val searchResults: StateFlow> get() = _searchResults + private val _savedSearchKeywords = MutableStateFlow>(emptyList()) + val savedSearchKeywords: StateFlow> get() = _savedSearchKeywords + + init { + getSavedSearchKeywords() + } + + fun getSearchResults(searchKeyword: SearchKeyword) { + viewModelScope.launch{ + _searchResults.value = searchRepository.Search(searchKeyword) + } + } + + fun saveSearchKeyword(searchKeyword: SearchKeyword) { + savedSearchKeywordRepository.saveSearchKeyword(searchKeyword) + getSavedSearchKeywords() + } + + fun getSavedSearchKeywords() { + _savedSearchKeywords.value = savedSearchKeywordRepository.getSavedSearchKeywords() + } + + fun delSavedSearchKeyword(searchKeyword: SearchKeyword) { + savedSearchKeywordRepository.delSavedSearchKeyword(searchKeyword) + getSavedSearchKeywords() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/SearchViewModelFactory.kt new file mode 100644 index 00000000..be8d1935 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchViewModelFactory.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class SearchViewModelFactory( + private val searchRepository: SearchRepository, + private val savedSearchKeywordRepository: SavedSearchKeywordRepository +) : + ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return SearchViewModel(searchRepository, savedSearchKeywordRepository) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchWindowActivity.kt b/app/src/main/java/campus/tech/kakao/map/SearchWindowActivity.kt new file mode 100644 index 00000000..fe45cecd --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchWindowActivity.kt @@ -0,0 +1,101 @@ +package campus.tech.kakao.map + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import androidx.appcompat.app.AppCompatActivity + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import campus.tech.kakao.map.databinding.ActivitySearchWindowBinding +import kotlinx.coroutines.launch + +class SearchWindowActivity : AppCompatActivity() { + + private lateinit var binding: ActivitySearchWindowBinding + private lateinit var viewModel: SearchViewModel + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySearchWindowBinding.inflate(layoutInflater) + setContentView(binding.root) + + val searchRepository = SearchRepository() + val savedSearchKeywordRepository = SavedSearchKeywordRepository(this) + val viewModelProviderFactory = + SearchViewModelFactory(searchRepository, savedSearchKeywordRepository) + viewModel = + ViewModelProvider(this, viewModelProviderFactory)[SearchViewModel::class.java] + + delSearchKeywordListener() + detectSearchWindowChangedListener() + displaySearchResults() + displaySavedSearchKeywords() + } + + private fun delSearchKeywordListener() { + binding.delSearchKeyword.setOnClickListener { + binding.searchWindow.text = null + } + } + + private fun detectSearchWindowChangedListener() { + binding.searchWindow.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 searchKeyWord = SearchKeyword(s.toString()) + viewModel.getSearchResults(searchKeyWord) + } + + override fun afterTextChanged(s: Editable?) { + + } + }) + } + + private fun displaySearchResults() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.searchResults.collect { + if (it.isEmpty()) { + showView(binding.emptySearchResults, true) + showView(binding.searchResultsList, false) + } else { + showView(binding.emptySearchResults, false) + showView(binding.searchResultsList, true) + binding.searchResultsList.adapter = + SearchResultsAdapter(it, layoutInflater, viewModel::saveSearchKeyword) + binding.searchResultsList.layoutManager = + LinearLayoutManager(this@SearchWindowActivity) + } + } + } + } + } + + private fun displaySavedSearchKeywords() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.savedSearchKeywords.collect { + binding.savedSearchKeywordsList.adapter = + SavedSearchKeywordsAdapter(it, layoutInflater, viewModel::delSavedSearchKeyword) + binding.savedSearchKeywordsList.layoutManager = LinearLayoutManager( + this@SearchWindowActivity, + LinearLayoutManager.HORIZONTAL, + false + ) + } + } + } + } + + private fun showView(view: View, isShow: Boolean) { + view.visibility = if (isShow) View.VISIBLE else View.GONE + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/StoreInfoDBHelper.kt b/app/src/main/java/campus/tech/kakao/map/StoreInfoDBHelper.kt new file mode 100644 index 00000000..1c834517 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/StoreInfoDBHelper.kt @@ -0,0 +1,30 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.provider.BaseColumns + +private const val SQL_SEARCH_KEYWORD_CREATE_ENTRIES = + "CREATE TABLE ${SearchKeywordEntry.TABLE_NAME} (" + + "${BaseColumns._ID} INTEGER PRIMARY KEY," + + "${SearchKeywordEntry.SEARCH_KEYWORD} TEXT UNIQUE)" + +private const val SQL_SEARCH_WORD_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${SearchKeywordEntry.TABLE_NAME}" + +class StoreInfoDBHelper(context: Context) : + SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + companion object { + const val DATABASE_NAME = "SearchKeyword.db" + const val DATABASE_VERSION = 1 + } + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL(SQL_SEARCH_KEYWORD_CREATE_ENTRIES) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL(SQL_SEARCH_WORD_DELETE_ENTRIES) + onCreate(db) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/magnifying_glass.xml b/app/src/main/res/drawable/magnifying_glass.xml new file mode 100644 index 00000000..38508916 --- /dev/null +++ b/app/src/main/res/drawable/magnifying_glass.xml @@ -0,0 +1,12 @@ + + + + +