diff --git a/README.md b/README.md index e0f925ff..d91469f0 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ -# android-map-search +# 3주차 - android-map-search +## step1 구현할 기능 목록 + +- [x] 앱 키를 외부에 노출하지 않도록 만드는 기능 +- [x] 카카오 API를 통해 데이터를 요청하는 기능 +- [x] 받은 데이터를 가공하는 기능 +- [x] 데이터를 RecyclerView 형태로 띄워주는 기능 + +--------------------------- +## step2 구현할 기능 목록 + +- [ ] 카카오 맵 sdk를 통해 카카오 맵을 띄우는 기능 +- [ ] 검색창을 선택하면 검색 화면으로 넘어가는 기능 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 12f04a22..2a518af7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,6 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -7,6 +10,7 @@ android { namespace = "campus.tech.kakao.map" compileSdk = 34 + defaultConfig { applicationId = "campus.tech.kakao.map" minSdk = 26 @@ -14,7 +18,12 @@ android { versionCode = 1 versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "KAKAO_LOCAL_API_KEY", gradleLocalProperties(rootDir, providers).getProperty("KAKAO_LOCAL_API_KEY")) + buildConfigField("String", "KAKAO_BASE_URL", gradleLocalProperties(rootDir, providers).getProperty("KAKAO_BASE_URL")) + buildConfigField("String", "KAKAO_NATIVE_APP_KEY", gradleLocalProperties(rootDir, providers).getProperty("KAKAO_NATIVE_APP_KEY")) + } buildTypes { @@ -36,7 +45,9 @@ android { buildFeatures { viewBinding = true + buildConfig = true } + } dependencies { @@ -49,8 +60,9 @@ dependencies { implementation("androidx.datastore:datastore-preferences:1.0.0") implementation("com.squareup.retrofit2:retrofit:2.11.0") implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("com.kakao.maps.open:android:2.9.5") implementation("androidx.activity:activity:1.8.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") -} +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 930d7f16..d9c93d60 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,9 +13,13 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Map" + android:usesCleartextTraffic="true" tools:targetApi="31"> + @@ -25,4 +29,4 @@ - + \ 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 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/adapter/PlaceViewAdapter.kt b/app/src/main/java/campus/tech/kakao/map/adapter/PlaceViewAdapter.kt index 32bda2f5..d7651da6 100644 --- a/app/src/main/java/campus/tech/kakao/map/adapter/PlaceViewAdapter.kt +++ b/app/src/main/java/campus/tech/kakao/map/adapter/PlaceViewAdapter.kt @@ -33,7 +33,6 @@ class PlaceViewAdapter( class PlaceDiffCallBack : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Place, newItem: Place): Boolean { - Log.d("testt", "areItemsTheSame: ${oldItem === newItem}") return oldItem === newItem } @@ -60,7 +59,8 @@ class PlaceViewHolder(itemView: View, val listener: OnClickPlaceListener) : fun bind(place : Place){ currentPlace = place name.text = place.name - location.text = place.location - category.text = place.category + location.text = place.location ?: "" + Log.d("testt", "입력값 : " + location.text.toString()) + category.text = place.category ?: "" } } diff --git a/app/src/main/java/campus/tech/kakao/map/adapter/SavedPlaceViewAdapter.kt b/app/src/main/java/campus/tech/kakao/map/adapter/SavedPlaceViewAdapter.kt index fc955d7a..6cc9b6c1 100644 --- a/app/src/main/java/campus/tech/kakao/map/adapter/SavedPlaceViewAdapter.kt +++ b/app/src/main/java/campus/tech/kakao/map/adapter/SavedPlaceViewAdapter.kt @@ -34,12 +34,10 @@ class SavedPlaceViewAdapter( } class SavedPlaceDiffCallBack : DiffUtil.ItemCallback() { - // 같은 Item인지 override fun areItemsTheSame(oldItem: SavedPlace, newItem: SavedPlace): Boolean { return oldItem === newItem } - // 같은 내용물을 가지고 있는지. 여기서는 name override fun areContentsTheSame(oldItem: SavedPlace, newItem: SavedPlace): Boolean { return oldItem.name == newItem.name } 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 82df63f2..bbc3ff9d 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,7 +1,13 @@ package campus.tech.kakao.map.model +import com.google.gson.annotations.SerializedName + +data class ResultSearch( + val documents: List +) + data class Place( - val name : String, - val location : String, - val category : String -) \ No newline at end of file + @SerializedName("place_name") val name : String, + @SerializedName("road_address_name") val location : String?, + @SerializedName("category_group_name") val category : String? +) diff --git a/app/src/main/java/campus/tech/kakao/map/repository/KakaoApiDataSource.kt b/app/src/main/java/campus/tech/kakao/map/repository/KakaoApiDataSource.kt new file mode 100644 index 00000000..f569d186 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/KakaoApiDataSource.kt @@ -0,0 +1,53 @@ +package campus.tech.kakao.map.repository + +import android.util.Log +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.model.Place +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.create + +class KakaoApiDataSource { + object KakaoRetrofitInstance { + + val kakaoLocalApi : KakaoLocalApi = getApiClient().create() + + private fun getApiClient(): Retrofit { + return Retrofit.Builder() + .baseUrl(BuildConfig.KAKAO_BASE_URL) + .client(provideOkHttpClient(AppInterceptor())) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + private fun provideOkHttpClient(interceptor: AppInterceptor): OkHttpClient + = OkHttpClient.Builder().run { + addInterceptor(interceptor) + build() + } + + class AppInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain) : okhttp3.Response = with(chain) { + val newRequest = request().newBuilder() + .addHeader("Authorization", BuildConfig.KAKAO_LOCAL_API_KEY) + .build() + proceed(newRequest) + } + } + } + + suspend fun getPlaceData(text: String) : List { + val emptyList = listOf() + val kakaoApi = KakaoRetrofitInstance.kakaoLocalApi + return try{ + val placeList = kakaoApi.getPlaceData(text) + Log.d("coroutineTest", "return") + placeList.documents ?: emptyList + } catch (e : Exception){ + Log.d("coroutineTest", e.toString()) + emptyList + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/KakaoLocalApi.kt b/app/src/main/java/campus/tech/kakao/map/repository/KakaoLocalApi.kt new file mode 100644 index 00000000..6f399ed5 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/KakaoLocalApi.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map.repository + +import campus.tech.kakao.map.model.ResultSearch +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface KakaoLocalApi { + @GET("v2/local/search/keyword.json") + suspend fun getPlaceData( + @Query("query") query: String + ): ResultSearch + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/PlaceRepository.kt index 48c39147..d332b2a4 100644 --- a/app/src/main/java/campus/tech/kakao/map/repository/PlaceRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/repository/PlaceRepository.kt @@ -6,6 +6,7 @@ import campus.tech.kakao.map.db.PlaceDBHelper import campus.tech.kakao.map.model.Place class PlaceRepository (val dbHelper: PlaceDBHelper){ + val kakaoApiDataSource = KakaoApiDataSource() fun getAllPlace() : List{ val cursor = dbHelper.readPlaceData() val placeList = mutableListOf() @@ -31,7 +32,10 @@ class PlaceRepository (val dbHelper: PlaceDBHelper){ } fun writePlace(place: Place){ - dbHelper.insertPlaceData(place.name, place.location, place.category) + val name = place.name + val location = place.location ?: "" + val category = place.category ?: "" + dbHelper.insertPlaceData(name, location, category) } fun getPlaceWithCategory(category : String): List{ @@ -58,4 +62,9 @@ class PlaceRepository (val dbHelper: PlaceDBHelper){ cursor.close() return placeList } + + suspend fun getKakaoLocalPlaceData(text : String) : List{ + val placeList = kakaoApiDataSource.getPlaceData(text) + return placeList + } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/MapActivity.kt new file mode 100644 index 00000000..74f08966 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/MapActivity.kt @@ -0,0 +1,82 @@ +package campus.tech.kakao.map.view + +import android.app.ActivityOptions +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.R +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 + + +class MapActivity : AppCompatActivity() { + + lateinit var map : MapView + lateinit var inputField : EditText + lateinit var searchIcon : ImageView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_map) + + initVar() + initSDK() + initMapView() + initClickListener() + } + + private fun initVar(){ + inputField = findViewById(R.id.input_search_field) + searchIcon = findViewById(R.id.search_icon) + bringFrontSearchField() + } + + private fun initSDK(){ + KakaoMapSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY) + } + + private fun initMapView(){ + map = findViewById(R.id.map_view) + map.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + Log.d("testt", "MapDestroy") + } + + override fun onMapError(error: Exception) { + Log.d("testt", error.toString()) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + Log.d("testt", "MapReady") + } + }) + + } + + private fun bringFrontSearchField(){ + inputField.bringToFront() + searchIcon.bringToFront() + } + + private fun initClickListener(){ + inputField.setOnClickListener{ + moveSearchPage(it) + } + } + + private fun moveSearchPage(view : View){ + val intent = Intent(this, SearchActivity::class.java) + val options = ActivityOptions.makeSceneTransitionAnimation( + this, view, "inputFieldTransition") + startActivity(intent, options.toBundle()) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt similarity index 83% rename from app/src/main/java/campus/tech/kakao/map/view/MainActivity.kt rename to app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt index 85a37374..b77e7d43 100644 --- a/app/src/main/java/campus/tech/kakao/map/view/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt @@ -1,16 +1,19 @@ package campus.tech.kakao.map.view +import android.content.Context import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.util.Log import android.view.View +import android.view.inputmethod.InputMethodManager 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.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import campus.tech.kakao.map.R @@ -23,9 +26,10 @@ import campus.tech.kakao.map.repository.PlaceRepository import campus.tech.kakao.map.repository.SavedPlaceRepository import campus.tech.kakao.map.viewmodel.MainActivityViewModel import campus.tech.kakao.map.viewmodel.ViewModelFactory +import kotlinx.coroutines.launch -class MainActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlaceListener { +class SearchActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlaceListener { lateinit var noResultText: TextView lateinit var inputSearchField: EditText lateinit var viewModel: MainActivityViewModel @@ -40,27 +44,12 @@ class MainActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlac override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + setContentView(R.layout.activity_search) initVar() initListeners() initRecyclerViews() - - viewModel.place.observe(this, Observer { - Log.d("readData", "검색창 결과 변경 감지") - val placeList = viewModel.place.value - Log.d("testt", "${placeList}") - searchRecyclerViewAdapter.submitList(placeList) - if (placeList?.isEmpty() == true) noResultText.visibility = View.VISIBLE - else noResultText.visibility = View.INVISIBLE - }) - - viewModel.savedPlace.observe(this, Observer { - Log.d("readData", "저장된 장소들 변경 감지") - val savedPlace = viewModel.savedPlace.value - savedPlaceRecyclerViewAdapter.submitList(savedPlace) - if (savedPlace?.isEmpty() == true) savedPlaceRecyclerView.visibility = View.GONE - else savedPlaceRecyclerView.visibility = View.VISIBLE - }) + initObserver() + inputSearchField.requestFocus() } override fun deleteSavedPlace(savedPlace: SavedPlace, position: Int) { @@ -98,6 +87,9 @@ class MainActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlac searchDeleteButton.setOnClickListener { inputSearchField.setText("") inputSearchField.clearFocus() + inputSearchField.parent.clearChildFocus(inputSearchField) + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(window.decorView.applicationWindowToken, 0) } } @@ -111,8 +103,11 @@ class MainActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlac override fun afterTextChanged(searchText: Editable?) { val text = searchText.toString() - // 검색어를 입력할 때마다 place의 값이 바뀌어 notify가 계속 호출되는 문제? - viewModel.getPlaceWithCategory(text) + Log.d("inputField", "text : ${text}") + Log.d("coroutine", "입력변경") + lifecycleScope.launch { + viewModel.getKakaoLocalData(text) + } } }) } @@ -136,6 +131,32 @@ class MainActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlac savedPlaceRecyclerView.adapter = savedPlaceRecyclerViewAdapter } + fun initObserver(){ + initPlaceObserver() + initSavedPlaceObserver() + } + + fun initPlaceObserver(){ + viewModel.place.observe(this, Observer { + Log.d("readData", "검색창 결과 변경 감지") + val placeList = viewModel.place.value + Log.d("testt", "${placeList}") + searchRecyclerViewAdapter.submitList(placeList) + if (placeList?.isEmpty() == true) noResultText.visibility = View.VISIBLE + else noResultText.visibility = View.INVISIBLE + }) + } + + fun initSavedPlaceObserver(){ + viewModel.savedPlace.observe(this, Observer { + Log.d("readData", "저장된 장소들 변경 감지") + val savedPlace = viewModel.savedPlace.value + savedPlaceRecyclerViewAdapter.submitList(savedPlace) + if (savedPlace?.isEmpty() == true) savedPlaceRecyclerView.visibility = View.GONE + else savedPlaceRecyclerView.visibility = View.VISIBLE + }) + } + override fun onDestroy() { dbHelper.close() super.onDestroy() diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/MainActivityViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/MainActivityViewModel.kt index 052c6a73..5a63cb6e 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/MainActivityViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MainActivityViewModel.kt @@ -1,5 +1,6 @@ package campus.tech.kakao.map.viewmodel +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -7,7 +8,7 @@ import campus.tech.kakao.map.model.Place import campus.tech.kakao.map.model.SavedPlace import campus.tech.kakao.map.repository.PlaceRepository import campus.tech.kakao.map.repository.SavedPlaceRepository -import java.util.Locale.Category + class MainActivityViewModel( private val placeRepository: PlaceRepository, @@ -17,20 +18,22 @@ class MainActivityViewModel( private val _savedPlace = MutableLiveData>() val place: LiveData> get() = _place val savedPlace: LiveData> get() = _savedPlace - init{ + + init { getSavedPlace() getPlaceWithCategory("") } + fun getPlace() { - _place.postValue(placeRepository.getAllPlace()) + _place.value = (placeRepository.getAllPlace()) } fun getPlaceWithCategory(category: String) { - _place.postValue(placeRepository.getPlaceWithCategory(category)) + _place.value = (placeRepository.getPlaceWithCategory(category)) } fun getSavedPlace() { - _savedPlace.postValue(savedPlaceRepository.getAllSavedPlace()) + _savedPlace.value = (savedPlaceRepository.getAllSavedPlace()) } fun savePlace(place: Place) { @@ -38,10 +41,18 @@ class MainActivityViewModel( getSavedPlace() } - fun deleteSavedPlace(savedPlace: SavedPlace){ + fun deleteSavedPlace(savedPlace: SavedPlace) { savedPlaceRepository.deleteSavedPlace(savedPlace) getSavedPlace() } + suspend fun getKakaoLocalData(text: String) { + Log.d("coroutineTest", "getKakaoLocalData") + if (text.isNotEmpty()) { + val placeList = placeRepository.getKakaoLocalPlaceData(text) + Log.d("coroutineTest", "getKakaoLocalDataReturn") + _place.value = (placeList) + } else _place.value = listOf() + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml new file mode 100644 index 00000000..333d048a --- /dev/null +++ b/app/src/main/res/layout/activity_map.xml @@ -0,0 +1,45 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_search.xml similarity index 70% rename from app/src/main/res/layout/activity_main.xml rename to app/src/main/res/layout/activity_search.xml index 9710b75c..7b4ae58d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -7,7 +7,8 @@ android:focusableInTouchMode="true" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".view.MainActivity"> + android:background="@color/white" + tools:context=".view.SearchActivity"> + + + + + diff --git a/app/src/main/res/layout/place_item.xml b/app/src/main/res/layout/place_item.xml index 94f5b65a..66edb168 100644 --- a/app/src/main/res/layout/place_item.xml +++ b/app/src/main/res/layout/place_item.xml @@ -22,7 +22,7 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toEndOf="@id/place_image" android:text="장소1" - android:textSize="24sp" + android:textSize="22sp" android:textStyle="bold" android:layout_marginTop="10dp" android:layout_marginStart="10dp" diff --git a/app/src/main/res/layout/saved_place_item.xml b/app/src/main/res/layout/saved_place_item.xml index 59b85bf7..fbba2754 100644 --- a/app/src/main/res/layout/saved_place_item.xml +++ b/app/src/main/res/layout/saved_place_item.xml @@ -1,6 +1,7 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 05ed4b9e..e00c56c1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,6 +3,7 @@