From f53f1e5281207af0812457c3b41bdf11e445c32f Mon Sep 17 00:00:00 2001 From: suminjang <127755029+sumintnals@users.noreply.github.com> Date: Tue, 23 Jul 2024 19:34:23 +0900 Subject: [PATCH 1/4] =?UTF-8?q?docs(README):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 73ba54e1..55750f2a 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ # android-map-refactoring + +### 이전 코드 가져오기 +- 위치 이동 코드를 옮겨 온다. From fa95882a9f43cb84201ff8c6fd3c8926264511b5 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 24 Jul 2024 02:27:54 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(Map):=20=EC=9D=B4=EC=A0=84=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 20 +- .../campus/tech/kakao/map/MainActivityTest.kt | 97 ++++++++++ .../kakao/map/RecyclerViewIdlingResource.kt | 83 ++++++++ .../tech/kakao/map/SearchActivityTest.kt | 110 +++++++++++ app/src/main/AndroidManifest.xml | 11 ++ .../java/campus/tech/kakao/map/Constans.kt | 6 + .../java/campus/tech/kakao/map/DBHelper.kt | 54 ++++++ .../campus/tech/kakao/map/ErrorActivity.kt | 35 ++++ .../campus/tech/kakao/map/MainActivity.kt | 97 +++++++++- .../campus/tech/kakao/map/MainViewModel.kt | 24 +++ .../campus/tech/kakao/map/MapApplication.kt | 19 ++ .../campus/tech/kakao/map/MapRepository.kt | 11 ++ .../main/java/campus/tech/kakao/map/Place.kt | 43 +++++ .../campus/tech/kakao/map/PlaceAdapter.kt | 67 +++++++ .../campus/tech/kakao/map/PlaceContract.kt | 10 + .../tech/kakao/map/PreferenceManager.kt | 44 +++++ .../campus/tech/kakao/map/RetrofitInstance.kt | 12 ++ .../tech/kakao/map/RetrofitRepository.kt | 29 +++ .../campus/tech/kakao/map/RetrofitService.kt | 16 ++ .../campus/tech/kakao/map/SearchActivity.kt | 140 ++++++++++++++ .../campus/tech/kakao/map/SearchHistory.kt | 6 + .../tech/kakao/map/SearchHistoryAdapter.kt | 50 +++++ .../campus/tech/kakao/map/SearchViewModel.kt | 97 ++++++++++ .../campus/tech/kakao/map/ViewModelFactory.kt | 22 +++ app/src/main/res/drawable/magnifyingglass.xml | 14 ++ app/src/main/res/drawable/map_marker.xml | 9 + app/src/main/res/drawable/map_pin.png | Bin 0 -> 2111 bytes app/src/main/res/drawable/refresh.xml | 15 ++ .../drawable/search_history_background.xml | 9 + app/src/main/res/drawable/xmark.xml | 15 ++ app/src/main/res/layout/activity_error.xml | 41 ++++ app/src/main/res/layout/activity_main.xml | 84 +++++++- app/src/main/res/layout/activity_search.xml | 61 ++++++ app/src/main/res/layout/place_item.xml | 51 +++++ .../main/res/layout/search_history_item.xml | 29 +++ app/src/main/res/values/strings.xml | 3 + .../tech/kakao/map/MainViewModelTest.kt | 99 ++++++++++ .../tech/kakao/map/SearchViewModelTest.kt | 182 ++++++++++++++++++ settings.gradle.kts | 1 + 39 files changed, 1705 insertions(+), 11 deletions(-) create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/MainActivityTest.kt create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/RecyclerViewIdlingResource.kt create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/Constans.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/DBHelper.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/ErrorActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/MainViewModel.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/MapApplication.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/MapRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/Place.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/PlaceAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/PlaceContract.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/PreferenceManager.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/RetrofitInstance.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/RetrofitService.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchHistory.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/SearchHistoryAdapter.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/ViewModelFactory.kt create mode 100644 app/src/main/res/drawable/magnifyingglass.xml create mode 100644 app/src/main/res/drawable/map_marker.xml create mode 100644 app/src/main/res/drawable/map_pin.png create mode 100644 app/src/main/res/drawable/refresh.xml create mode 100644 app/src/main/res/drawable/search_history_background.xml create mode 100644 app/src/main/res/drawable/xmark.xml create mode 100644 app/src/main/res/layout/activity_error.xml create mode 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/layout/place_item.xml create mode 100644 app/src/main/res/layout/search_history_item.xml create mode 100644 app/src/test/java/campus/tech/kakao/map/MainViewModelTest.kt create mode 100644 app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c1a15f2..b1e726da 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -19,6 +21,8 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY")) + buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY")) } buildTypes { @@ -41,10 +45,18 @@ android { buildFeatures { dataBinding = true buildConfig = true + viewBinding = true } } dependencies { + implementation("androidx.test.ext:junit-ktx:1.2.1") + kapt("com.android.databinding:compiler:3.1.4") + implementation("androidx.fragment:fragment-ktx:1.8.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3") + implementation("com.google.code.gson:gson:2.11.0") + implementation("com.kakao.sdk:v2-all:2.20.3") implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") @@ -53,7 +65,7 @@ dependencies { implementation("androidx.recyclerview:recyclerview:1.3.2") 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("com.kakao.maps.open:android:2.9.7") implementation("androidx.activity:activity-ktx:1.9.0") implementation("androidx.test:core-ktx:1.6.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") @@ -77,4 +89,10 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1") androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1") + androidTestImplementation("androidx.test.espresso:espresso-contrib:3.3.0") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.10") + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("org.robolectric:robolectric:4.11.1") } +fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key) \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MainActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MainActivityTest.kt new file mode 100644 index 00000000..0c3ff875 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MainActivityTest.kt @@ -0,0 +1,97 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.kakao.vectormap.MapLifeCycleCallback +import com.kakao.vectormap.MapView +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) +class MainActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + private lateinit var sharedPreferences: SharedPreferences + + @Before + fun setUp() { + sharedPreferences = ApplicationProvider.getApplicationContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) + Intents.init() + } + + @After + fun tearDown() { + Intents.release() + } + + @Test + fun `앱_실행_직후_메인에서_보여주는_지도는_가장_최근검색어의_위치_혹은_기본값`() { + // given + val editor = sharedPreferences.edit() + editor.putString(Constants.SEARCH_HISTORY_KEY, "[{\"document\":{\"x\":\"126.9780\",\"y\":\"37.5665\"}}]") + editor.apply() + + // when + activityRule.scenario.moveToState(Lifecycle.State.RESUMED) + + // then + onView(withId(R.id.map_view)).check(matches(isDisplayed())) + + editor.clear().apply() + } + + @Test + fun `앱_실행_직후_최근검색어가_없는_경우_기본위치_확인`() { + // given + val editor = sharedPreferences.edit() + editor.clear().apply() + + // when + activityRule.scenario.moveToState(Lifecycle.State.RESUMED) + + // then + onView(withId(R.id.map_view)).check(matches(isDisplayed())) + } + + @Test + fun `지도_호출_오류시_오류화면_이동`() { + // given + activityRule.scenario.onActivity { activity -> + runOnUiThread { + val mapView = activity.findViewById(R.id.map_view) + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + // 지도 파괴 시 처리 + } + + override fun onMapError(error: Exception) { + // when + activity.startActivity( + Intent(activity, ErrorActivity::class.java) + .putExtra("Error", "Test Error Message") + ) + } + }, null) + } + } + + // then + Intents.intended(hasComponent(ErrorActivity::class.java.name)) + } +} diff --git a/app/src/androidTest/java/campus/tech/kakao/map/RecyclerViewIdlingResource.kt b/app/src/androidTest/java/campus/tech/kakao/map/RecyclerViewIdlingResource.kt new file mode 100644 index 00000000..2b895b1d --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/RecyclerViewIdlingResource.kt @@ -0,0 +1,83 @@ +package campus.tech.kakao.map + +import android.util.Log +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.IdlingResource +import androidx.test.espresso.idling.CountingIdlingResource +import java.util.concurrent.atomic.AtomicBoolean + +class RecyclerViewIdlingResource( + private val recyclerView: RecyclerView +) : IdlingResource { + + @Volatile + private var callback: IdlingResource.ResourceCallback? = null + + private val isIdleNow = AtomicBoolean(false) + + init { + recyclerView.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + val itemCount = recyclerView.adapter?.itemCount ?: 0 + Log.d("RecyclerViewIdlingResource", "onChanged: itemCount = $itemCount") + if (itemCount > 0) { + isIdleNow.set(true) + callback?.onTransitionToIdle() + } + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + onChanged() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + onChanged() + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + onChanged() + } + }) + } + + override fun getName(): String { + return this::class.java.name + } + + override fun isIdleNow(): Boolean { + val idle = isIdleNow.get() + Log.d("RecyclerViewIdlingResource", "isIdleNow: $idle") + return idle + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + this.callback = callback + if (isIdleNow.get()) { + callback?.onTransitionToIdle() + } + } +} + +class RecyclerViewCountingIdlingResource( + private val recyclerView: RecyclerView, + private val resource: CountingIdlingResource +) { + + init { + resource.increment() + recyclerView.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + if (recyclerView.adapter?.itemCount ?: 0 > 0) { + resource.decrement() + } + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (recyclerView.adapter?.itemCount ?: 0 > 0) { + resource.decrement() + } + } + }) + } +} + diff --git a/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt new file mode 100644 index 00000000..1f9690b5 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt @@ -0,0 +1,110 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.view.LayoutInflater +import androidx.core.content.ContextCompat.startActivity +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.IdlingResource +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.gson.Gson +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +class SearchActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(SearchActivity::class.java) + + private lateinit var sharedPreferences: SharedPreferences + private var idlingResource: IdlingResource? = null + private lateinit var placeAdapter: PlaceAdapter + + @Before + fun setUp() { + sharedPreferences = ApplicationProvider.getApplicationContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) + Intents.init() + } + + @After + fun tearDown() { + Intents.release() + idlingResource?.let { IdlingRegistry.getInstance().unregister(it) } + } + + @Test + fun `검색창_X누르면_지워진다`() { + onView(withId(R.id.search)).perform(typeText("cafe")) + onView(withId(R.id.xmark)).perform(click()) + onView(withId(R.id.search)).check(matches(withText(""))) + } + +// @Test +// fun `검색결과_누르면_메인액티비티로_이동한다`() { +// // Activity 실행 시점에 RecyclerView의 IdlingResource 등록 +// activityRule.scenario.onActivity { activity -> +// val recyclerView = activity.findViewById(R.id.placeResult) +// idlingResource = RecyclerViewIdlingResource(recyclerView) +// IdlingRegistry.getInstance().register(idlingResource) +// +// // 수동으로 RecyclerView에 데이터 삽입 +// val testData = listOf( +// Document( +// addressName = "서울 강남구 삼성동 159", +// categoryGroupCode = "", +// categoryGroupName = "", +// categoryName = "가정,생활 > 문구,사무용품 > 디자인문구 > 카카오프렌즈", +// distance = "418", +// id = "26338954", +// phone = "02-6002-1880", +// placeName = "카카오프렌즈 코엑스점", +// placeUrl = "http://place.map.kakao.com/26338954", +// roadAddressName = "서울 강남구 영동대로 513", +// x = "127.05902969025047", +// y = "37.51207412593136" +// ) +// ) +// +// val placeAdapter = PlaceAdapter( +// testData, +// LayoutInflater.from(activity), +// object : PlaceAdapter.OnItemClickListener { +// override fun onItemClick(position: Int) { +// val item = placeAdapter.getItem(position) +// val intent = Intent(activity, MainActivity::class.java) +// intent.putExtra("longitude", item.x) +// intent.putExtra("latitude", item.y) +// intent.putExtra("name", item.placeName) +// intent.putExtra("address", item.addressName) +// activity.startActivity(intent) +// } +// }) +// +// recyclerView.adapter = placeAdapter +// placeAdapter.notifyDataSetChanged() +// } +// +// // RecyclerView 데이터 로드 확인 +// onView(withId(R.id.placeResult)).check(matches(hasMinimumChildCount(1))) +// +// // 첫 번째 아이템 클릭 +// onView(withId(R.id.placeResult)).perform(actionOnItemAtPosition(0, click())) +// +// // MainActivity로 이동 확인 +// Intents.intended(hasComponent(MainActivity::class.java.name)) +// } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..99800678 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,12 @@ + + + + + @@ -21,6 +29,9 @@ + diff --git a/app/src/main/java/campus/tech/kakao/map/Constans.kt b/app/src/main/java/campus/tech/kakao/map/Constans.kt new file mode 100644 index 00000000..d9f11c44 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Constans.kt @@ -0,0 +1,6 @@ +package campus.tech.kakao.map + +object Constants { + const val SEARCH_HISTORY_KEY = "searchHistory" + const val PREFERENCE_NAME = "preference_name" +} \ 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..e451de63 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/DBHelper.kt @@ -0,0 +1,54 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class DBHelper (context: Context): SQLiteOpenHelper(context, "place.db", null, 1) { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL( + "CREATE TABLE IF NOT EXISTS ${PlaceContract.TABLE_NAME} ("+ + "${PlaceContract.TABLE_COLUMN_NAME} VARCHAR(30) NOT NULL,"+ + "${PlaceContract.TABLE_COLUMN_ADDRESS} VARCHAR(50) NOT NULL,"+ + "${PlaceContract.TABLE_COLUMN_CATEGORY} VARCHAR(30) NOT NULL"+ + ");" + ) + insertDummyData(db) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL("DROP TABLE IF EXISTS ${PlaceContract.TABLE_NAME}") + onCreate(db) + } + + fun insert(db: SQLiteDatabase, place: Place) { + val sql = " INSERT INTO " + + "${PlaceContract.TABLE_NAME}("+ + "${PlaceContract.TABLE_COLUMN_NAME}, ${PlaceContract.TABLE_COLUMN_ADDRESS}, ${PlaceContract.TABLE_COLUMN_CATEGORY})"+ + " VALUES(${place.name}, ${place.address}, ${place.category});" + + db.execSQL(sql) + } + + fun select(db: SQLiteDatabase, name:String, address: String, category: String) : String? { + return null + } + + private fun insertDummyData(db: SQLiteDatabase?) { + db?.let { + val categories = listOf("카페", "약국", "식당") + val baseAddress = "서울 성동구 성수동" + + categories.forEach { category -> + for (i in 1..15) { + val name = "$category $i" + val address = "$baseAddress $i" + val sql = "INSERT INTO ${PlaceContract.TABLE_NAME} (" + + "${PlaceContract.TABLE_COLUMN_NAME}, ${PlaceContract.TABLE_COLUMN_ADDRESS}, ${PlaceContract.TABLE_COLUMN_CATEGORY}) " + + "VALUES ('$name', '$address', '$category');" + it.execSQL(sql) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ErrorActivity.kt b/app/src/main/java/campus/tech/kakao/map/ErrorActivity.kt new file mode 100644 index 00000000..47d4dfcf --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ErrorActivity.kt @@ -0,0 +1,35 @@ +package campus.tech.kakao.map + +import android.content.Intent +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import campus.tech.kakao.map.databinding.ActivityErrorBinding +import campus.tech.kakao.map.databinding.ActivityMainBinding + +class ErrorActivity : AppCompatActivity() { + private lateinit var errorViewBinding: ActivityErrorBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + errorViewBinding = ActivityErrorBinding.inflate(layoutInflater) + enableEdgeToEdge() + setContentView(errorViewBinding.root) + + errorViewBinding.errorText.text = intent.getStringExtra("Error") + + errorViewBinding.refreshButton.setOnClickListener { + val intent = Intent(this@ErrorActivity, MainActivity::class.java) + startActivity(intent) + finish() + } + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finishAffinity() + } + }) + } +} \ 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..8e854543 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,106 @@ package campus.tech.kakao.map +import android.content.Intent import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.activity.viewModels 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.LatLng +import com.kakao.vectormap.MapLifeCycleCallback +import com.kakao.vectormap.MapView +import com.kakao.vectormap.label.Label +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles +import okhttp3.Address class MainActivity : AppCompatActivity() { + private lateinit var mainViewBinding: ActivityMainBinding + private lateinit var mapView: MapView + private val mainViewModel: MainViewModel by viewModels { + ViewModelFactory(applicationContext, MapApplication.prefs) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + mainViewBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(mainViewBinding.root) + + val latitude = intent?.getStringExtra("latitude")?.toDoubleOrNull() + val longitude = intent?.getStringExtra("longitude")?.toDoubleOrNull() + val name = intent?.getStringExtra("name") + val address = intent?.getStringExtra("address") + + mapView = mainViewBinding.mapView + + setUpMapView(mapView, longitude, latitude) + setBottomSheet(name, address) + + mainViewBinding.search.setOnClickListener { + val intent = Intent(this@MainActivity, SearchActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + startActivity(intent) + } + } + + override fun onPause() { + super.onPause() + mapView.pause() + } + + override fun onResume() { + super.onResume() + mapView.resume() + } + + private fun setUpMapView(mapView: MapView, longitude: Double?, latitude: Double?) { + + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + // 지도 파괴 시 처리 + } + + override fun onMapError(error: Exception) { + val intent = Intent(this@MainActivity, ErrorActivity::class.java) + intent.putExtra("Error", error.localizedMessage) + startActivity(intent) + } + + }, object : KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + val labelManager = kakaoMap.labelManager + val styles: LabelStyles? = labelManager?.addLabelStyles(LabelStyles.from(LabelStyle.from(R.drawable.map_pin))) + val options = LabelOptions.from(position).setStyles(styles) + val layer = kakaoMap.labelManager?.layer + val label = layer?.addLabel(options) + } + + override fun isVisible(): Boolean { + return true + } + + override fun getZoomLevel(): Int { + return 18 + } + + override fun getPosition(): LatLng { + return mainViewModel.setLocation(latitude, longitude) ?: super.getPosition() + } + }) } + + private fun setBottomSheet(name: String?, address: String?) { + if (!name.isNullOrEmpty() && !address.isNullOrEmpty()) { + mainViewBinding.place.text = name + mainViewBinding.address.text = address + mainViewBinding.mapBottomSheet.visibility = View.VISIBLE + } else { + mainViewBinding.mapBottomSheet.visibility = View.GONE + } + } + } diff --git a/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt b/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt new file mode 100644 index 00000000..02f93898 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map + +import androidx.lifecycle.ViewModel +import com.kakao.vectormap.LatLng + +class MainViewModel( + private val preferenceManager: PreferenceManager +) : ViewModel() { + + fun setLocation(latitude: Double? = null, longitude: Double? = null): LatLng? { + return if (latitude != null && longitude != null) { + LatLng.from(latitude, longitude) + } else { + val historyList = preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY) + if (historyList.isNullOrEmpty()) { + null + } else { + val historyLongitude = historyList[0].document.x.toDouble() + val historyLatitude = historyList[0].document.y.toDouble() + LatLng.from(historyLatitude, historyLongitude) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapApplication.kt b/app/src/main/java/campus/tech/kakao/map/MapApplication.kt new file mode 100644 index 00000000..ef5bbabd --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapApplication.kt @@ -0,0 +1,19 @@ +package campus.tech.kakao.map + +import android.app.Application +import android.content.SharedPreferences +import android.util.Log +import com.kakao.vectormap.KakaoMapSdk + +class MapApplication: Application() { + companion object { + lateinit var prefs: PreferenceManager + } + + override fun onCreate() { + val apiKey = getString(R.string.kakao_api_key) + prefs = PreferenceManager(applicationContext) + super.onCreate() + KakaoMapSdk.init(this, apiKey) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapRepository.kt b/app/src/main/java/campus/tech/kakao/map/MapRepository.kt new file mode 100644 index 00000000..4fe82e0f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapRepository.kt @@ -0,0 +1,11 @@ +package campus.tech.kakao.map + +import com.kakao.vectormap.LatLng + +class MapRepository { + + fun setPosition(longitude: Double, latitude: Double): LatLng { + return LatLng.from(longitude, latitude) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/Place.kt b/app/src/main/java/campus/tech/kakao/map/Place.kt new file mode 100644 index 00000000..31a20b3f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Place.kt @@ -0,0 +1,43 @@ +package campus.tech.kakao.map + +import com.google.gson.annotations.SerializedName + +data class Place ( + val name: String, + val address: String, + val category: String, +) + +// API +data class Location ( + @SerializedName("documents") val documents: List, + @SerializedName("meta") val meta: Meta +) + +data class Document ( + @SerializedName("address_name") val addressName: String, + @SerializedName("category_group_code") val categoryGroupCode: String, + @SerializedName("category_group_name") val categoryGroupName: String, + @SerializedName("category_name") val categoryName: String, + @SerializedName("distance") val distance: String, + @SerializedName("id") val id: String, + @SerializedName("phone") val phone: String, + @SerializedName("place_name") val placeName: String, + @SerializedName("place_url") val placeUrl: String, + @SerializedName("road_address_name") val roadAddressName: String, + @SerializedName("x") val x: String, + @SerializedName("y") val y: String +) + +data class Meta( + @SerializedName("is_end") val isEnd: Boolean, + @SerializedName("pageable_count") val pageableCount: Int, + @SerializedName("same_name") val sameName: SameName, + @SerializedName("total_count") val totalCount: Int +) + +data class SameName( + @SerializedName("keyword") val keyword: String, + @SerializedName("region") val region: List, + @SerializedName("selected_region") val selectedRegion: String +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/PlaceAdapter.kt b/app/src/main/java/campus/tech/kakao/map/PlaceAdapter.kt new file mode 100644 index 00000000..9643c8e1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/PlaceAdapter.kt @@ -0,0 +1,67 @@ +package campus.tech.kakao.map + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.BaseAdapter +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.RecyclerView + +class PlaceAdapter(var items: List, val inflater: LayoutInflater, var itemClickListener: OnItemClickListener): RecyclerView.Adapter() { + + interface OnItemClickListener { + fun onItemClick(position: Int) {} + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): PlaceAdapter.PlaceViewHolder { + val view = inflater.inflate(R.layout.place_item, parent, false) + return PlaceViewHolder(view) + } + + override fun onBindViewHolder(holder: PlaceAdapter.PlaceViewHolder, position: Int) { + holder.name.text = items[position].placeName + holder.address.text = items[position].addressName + holder.category.text = getLastCategory(items[position].categoryName) + } + + override fun getItemCount(): Int { + return items.size + } + + private fun getLastCategory(input: String): String { + val categories = input.split(">") + val lastCategory = categories.lastOrNull()?.trim() + + return lastCategory ?: "" + } + fun setData(searchResults: List) { + items = searchResults + notifyDataSetChanged() + } + + fun getItem(position: Int): Document { + return items[position] + } + + inner class PlaceViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { + val name: TextView + val address: TextView + val category: TextView + + init { + name = itemView.findViewById(R.id.place) + address = itemView.findViewById(R.id.address) + category = itemView.findViewById(R.id.category) + + itemView.setOnClickListener { + itemClickListener.onItemClick(absoluteAdapterPosition) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/PlaceContract.kt b/app/src/main/java/campus/tech/kakao/map/PlaceContract.kt new file mode 100644 index 00000000..1626e61b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/PlaceContract.kt @@ -0,0 +1,10 @@ +package campus.tech.kakao.map + +import android.provider.BaseColumns + +object PlaceContract: BaseColumns { + const val TABLE_NAME = "place" + const val TABLE_COLUMN_NAME = "name" + const val TABLE_COLUMN_ADDRESS = "address" + const val TABLE_COLUMN_CATEGORY = "category" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/PreferenceManager.kt b/app/src/main/java/campus/tech/kakao/map/PreferenceManager.kt new file mode 100644 index 00000000..8b313acd --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/PreferenceManager.kt @@ -0,0 +1,44 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.content.SharedPreferences +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken + +open class PreferenceManager(context: Context) { + private val prefs: SharedPreferences + = context.getSharedPreferences(Constants.PREFERENCE_NAME, Context.MODE_PRIVATE) + + fun deleteString(key: String) { + prefs.edit().remove(key).apply() + } + + open fun getArrayList(key: String): ArrayList { + val stringPrefs = prefs.getString(key, null) + return if (stringPrefs != null && stringPrefs != "[]") { + GsonBuilder().create().fromJson( + stringPrefs, object : TypeToken>() {}.type + ) + } else { + ArrayList() + } + } + + private fun setArrayList(key: String, list: ArrayList) { + val jsonString = GsonBuilder().create().toJson(list) + prefs.edit().putString(key, jsonString).apply() + } + + open fun deleteArrayListItem(key: String, index: Int) { + val list = getArrayList(key) + if (index >= 0 && index < list.size) { + list.removeAt(index) + setArrayList(key, list) + } + } + + open fun savePreference(key: String, history: SearchHistory, list: ArrayList) { + list.add(0, history) + setArrayList(key, list) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/RetrofitInstance.kt b/app/src/main/java/campus/tech/kakao/map/RetrofitInstance.kt new file mode 100644 index 00000000..87ca9170 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/RetrofitInstance.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitInstance { + val retrofitService = Retrofit.Builder() + .baseUrl("https://dapi.kakao.com/v2/local/search/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(RetrofitService::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt b/app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt new file mode 100644 index 00000000..9981706a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt @@ -0,0 +1,29 @@ +package campus.tech.kakao.map + +import android.util.Log +import campus.tech.kakao.map.RetrofitInstance.retrofitService +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +open class RetrofitRepository { + open suspend fun getPlace(query: String): List { + if (query.isEmpty()) { + return emptyList() + } + + return try { + val response: Response = retrofitService.getPlaces( + "KakaoAK " + BuildConfig.KAKAO_REST_API_KEY, + query + ) + if (response.isSuccessful) { + response.body()?.documents ?: emptyList() + } else { + emptyList() + } + } catch (e: Exception) { + emptyList() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/RetrofitService.kt b/app/src/main/java/campus/tech/kakao/map/RetrofitService.kt new file mode 100644 index 00000000..57cb9971 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/RetrofitService.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map + +import com.google.gson.internal.GsonBuildConfig +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface RetrofitService { + @GET("keyword") + suspend fun getPlaces( + @Header("Authorization") authorization: String = "KakaoAK "+BuildConfig.KAKAO_REST_API_KEY , + @Query("query") query: String + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt new file mode 100644 index 00000000..475b5e29 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt @@ -0,0 +1,140 @@ +package campus.tech.kakao.map + +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import campus.tech.kakao.map.databinding.ActivitySearchBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class SearchActivity : AppCompatActivity() { + private val viewModel: SearchViewModel by viewModels { + ViewModelFactory(applicationContext, MapApplication.prefs) + } + + private val placeAdapter: PlaceAdapter by lazy { + PlaceAdapter( + emptyList(), + LayoutInflater.from(this@SearchActivity), + object : + PlaceAdapter.OnItemClickListener { + override fun onItemClick(position: Int) { + val item = placeAdapter.getItem(position) + val searchHistory = SearchHistory(item.placeName, item) + viewModel.saveSearchHistory(searchHistory) + + val intent = Intent(this@SearchActivity, MainActivity::class.java) + intent.putExtra("longitude", item.x) + intent.putExtra("longitude", item.y) + intent.putExtra("name", item.placeName) + intent.putExtra("address", item.addressName) + intent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + startActivity(intent) + } + } + ) + } + + private val historyAdapter: HistoryAdapter by lazy { + HistoryAdapter( + emptyList(), + LayoutInflater.from(this@SearchActivity), + object : HistoryAdapter.OnItemClickListener { + override fun onItemClick(position: Int) { + val item = viewModel.searchHistoryList.value?.get(position) + if (item != null) { + searchViewBinding.search.setText(item.searchHistory) + } + } + override fun onXMarkClick(position: Int) { + viewModel.deleteSearchHistory(position) + } + } + ) + } + + private lateinit var searchViewBinding: ActivitySearchBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + searchViewBinding = ActivitySearchBinding.inflate(layoutInflater) + setContentView(searchViewBinding.root) + + setupRecyclerViews(searchViewBinding) + setupSearchEditText(searchViewBinding) + observeViewModel(searchViewBinding) + + searchViewBinding.xmark.setOnClickListener { + searchViewBinding.search.setText("") + } + } + + private fun setupRecyclerViews(mainBinding: ActivitySearchBinding) { + mainBinding.placeResult.apply { + layoutManager = LinearLayoutManager(this@SearchActivity, LinearLayoutManager.VERTICAL, false) + adapter = placeAdapter + } + + mainBinding.searchHistory.apply { + layoutManager = LinearLayoutManager(this@SearchActivity, LinearLayoutManager.HORIZONTAL, false) + adapter = historyAdapter + } + } + + private fun setupSearchEditText(mainBinding: ActivitySearchBinding) { + val searchEditText = mainBinding.search + val timeMillis = 300L + val debounce = debounce(timeMillis, CoroutineScope(Dispatchers.Main)) { query -> + viewModel.getPlace(query) } + + searchEditText.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) {} + + override fun afterTextChanged(s: Editable?) { + val searchText = s.toString().trim() + debounce(searchText) + } + }) + } + + private fun observeViewModel(mainBinding: ActivitySearchBinding) { + viewModel.searchHistoryList.observe(this@SearchActivity, Observer { + historyAdapter.setData(it) + }) + viewModel.getSearchHistoryList() + + viewModel.locationList.observe(this@SearchActivity, Observer { + placeAdapter.setData(it) + mainBinding.emptyMainText.visibility = if (it.isNullOrEmpty()) View.VISIBLE else View.GONE + }) + } + + private fun debounce( + timeMillis: Long = 500L, + coroutineScope: CoroutineScope, + block: (T) -> Unit + ): (T) -> Unit { + var debounceJob: Job? = null + return { + debounceJob?.cancel() + debounceJob = coroutineScope.launch { + delay(timeMillis) + block(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchHistory.kt b/app/src/main/java/campus/tech/kakao/map/SearchHistory.kt new file mode 100644 index 00000000..0f5d6d32 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchHistory.kt @@ -0,0 +1,6 @@ +package campus.tech.kakao.map + +data class SearchHistory( + val searchHistory: String, + val document: Document +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SearchHistoryAdapter.kt b/app/src/main/java/campus/tech/kakao/map/SearchHistoryAdapter.kt new file mode 100644 index 00000000..4275a0eb --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchHistoryAdapter.kt @@ -0,0 +1,50 @@ +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 +import campus.tech.kakao.map.databinding.SearchHistoryItemBinding + +class HistoryAdapter(var items: List, val inflater: LayoutInflater, var itemClickListener: OnItemClickListener) : RecyclerView.Adapter() { + + interface OnItemClickListener { + fun onItemClick(position: Int) + fun onXMarkClick(position: Int) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder { + val binding = SearchHistoryItemBinding.inflate(inflater, parent, false) + return HistoryViewHolder(binding) + } + + override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount(): Int { + return items.size + } + + fun setData(searchHistory: List) { + items = searchHistory + notifyDataSetChanged() + } + + inner class HistoryViewHolder(private val binding: SearchHistoryItemBinding) : RecyclerView.ViewHolder(binding.root) { + init { + itemView.setOnClickListener { + itemClickListener.onItemClick(absoluteAdapterPosition) + } + + binding.xmark.setOnClickListener { + itemClickListener.onXMarkClick(absoluteAdapterPosition) + } + } + fun bind(searchHistory: SearchHistory) { + binding.history.text = searchHistory.searchHistory + } + } +} 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..b5a95b7f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt @@ -0,0 +1,97 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SearchViewModel(context: Context) : ViewModel() { + private val dbHelper: DBHelper = DBHelper(context) + private val db = dbHelper.writableDatabase + private val preferenceManager = MapApplication.prefs + var repository = RetrofitRepository() + + private var _placeList = MutableLiveData>() + private val _searchHistoryList = MutableLiveData>() + private var _locationList = MutableLiveData>() + + init { + _searchHistoryList.value = getSearchHistory() + } + + val searchHistoryList: LiveData> + get() = _searchHistoryList + + val placeList: LiveData> + get() = _placeList + + val locationList: LiveData> + get() = _locationList + + fun insertPlace(place: Place) { + dbHelper.insert(db, place) + } + + override fun onCleared() { + super.onCleared() + if (db.isOpen) db.close() + } + + fun getSearchResult(searchText: String) { + if (searchText.isEmpty()) { + _placeList.postValue(emptyList()) + } else { + val rDb = dbHelper.readableDatabase + val places = mutableListOf() + val query = "SELECT * FROM ${PlaceContract.TABLE_NAME} WHERE ${PlaceContract.TABLE_COLUMN_NAME} LIKE ?" + val cursor = rDb.rawQuery(query, arrayOf("%$searchText%")) + + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + val name = cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.TABLE_COLUMN_NAME)) + val address = cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.TABLE_COLUMN_ADDRESS)) + val category = cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.TABLE_COLUMN_CATEGORY)) + val place = Place(name, address, category) + places.add(place) + } while (cursor.moveToNext()) + } + cursor.close() + } + _placeList.postValue(places) + } + } + + fun getSearchHistoryList() { + _searchHistoryList.value = getSearchHistory() + } + + private fun getSearchHistory(): ArrayList { + return preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY) + } + + fun saveSearchHistory(searchHistory: SearchHistory) { + val currentList = getSearchHistory() + preferenceManager.savePreference(Constants.SEARCH_HISTORY_KEY, searchHistory, currentList) + getSearchHistoryList() + } + + fun deleteSearchHistory(position: Int) { + preferenceManager.deleteArrayListItem(Constants.SEARCH_HISTORY_KEY, position) + getSearchHistoryList() + } + + fun getPlace(query: String) { + viewModelScope.launch { + val places = withContext(Dispatchers.IO) { + repository.getPlace(query) + } + _locationList.postValue(places) + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/ViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/ViewModelFactory.kt new file mode 100644 index 00000000..6cd90a41 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ViewModelFactory.kt @@ -0,0 +1,22 @@ +package campus.tech.kakao.map + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class ViewModelFactory(private val context: Context + ,private val preferenceManager: PreferenceManager) + : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(SearchViewModel::class.java)) { + return SearchViewModel(context) as T + } + else if (modelClass.isAssignableFrom(MainViewModel::class.java)) { + return MainViewModel(preferenceManager) as T + } + else { + throw IllegalArgumentException("Failed to create ViewModel : ${modelClass.name}") + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/magnifyingglass.xml b/app/src/main/res/drawable/magnifyingglass.xml new file mode 100644 index 00000000..e258e402 --- /dev/null +++ b/app/src/main/res/drawable/magnifyingglass.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/map_marker.xml b/app/src/main/res/drawable/map_marker.xml new file mode 100644 index 00000000..9ce300a5 --- /dev/null +++ b/app/src/main/res/drawable/map_marker.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/map_pin.png b/app/src/main/res/drawable/map_pin.png new file mode 100644 index 0000000000000000000000000000000000000000..5721b63ebb0b5914b2017ec44b23c6b44c6dba9a GIT binary patch literal 2111 zcmV-F2*CG=P)SKK~#7F-JD%) z6jc<*&%IqLv^*3sXksf{AQlOPVq&6bVjDC(iSUBmVj_JpK|+8;pDDg-0!CXN+a|KD zPb~0YNF*&9ki@U{9T*bolLM6P{ZQDCNoT&Z<=pP?MwB|f+n;ClS~ZmmH+stG@=VM z!Waw#=Oc-)4^03uWWk^v3r!10ZO9xWFvBmFU~uw4^6)SaK@!Z_-hqQmzzHa7D?-4E zu@(C$+9IW3Mtjc0fr%mAr;J~mQ_SWk6Jv)g2uR>y#(D>iWyg!jVbP{~xD;PX=1hdB>{I&rmF}dn- zx_e*hxTo63Q!sT9ii_uPc(nuSC={e~VbxRTyc}3D7;I?kC=>@QEyfNEo~m|S#bB`a zfjSDs#kR#UIczyr>6o%@%xKTR30Y?MLmd-iuw#GnaLFpUWH3wZ$~~xmEZG?EDAgF3 zJe35uJvC5uKzzQbvE(VW%$TJC;Sd;PMN=a*$`54j??+%7CU|@CxGJ{%AZL_pgmoRy z!M1m{J74pep86Ah{^lo`x&IFsK@NoF=k9`=@_#UC%T^i4wmqE{kGXYW5^i3+0Va?G z&hGpHXF(fd%9i$>8>@zbY?a4!?0N}|kP!vXJr@kx0tRXpY)ia^;)3C|@1p6_gf=C!LmACEz9cx@w?Aisxlf^q5=L$h#xY;kFSeG`~KHYGVZ zoM3EjcYy)0!S!Q{%bK>Qo!-j;@zM9sbm2EnFeXl;m*0COkS#9Q)|d##wzHBrb&QVF z$SvAhx3@vZ+gk#eQVfQJr`1i_jnCenfkzJ?!Qy-xD3Ox9iC<%*JzsVSPzQt9EW1`L z)`|-@N-*G8``#$H4GY1k?|y}Q*M0|8++|~yOnfCitX?h0xy^*qf>~?l z3lKG4d~1u6H5~YQ?5^Vw5Nu>kl|#avcRT<&#P* zKBlc9D4BaW2P!B9Gd=YeL>271RZt2hEvrtPZHpRruia6uWiS{E6u3TgRr%GS1Ru*S z)mc9w{*^-mP}<< zfB+2V`+;%t(XQ9~{$Y^rz8kXJW`S%|F+RZG5 z+oV0?EbjW`xB{oz0OvEOVe+UFSTc~>BH#*s<=G$Dc_1Whup-t_eW&n~6O3SD0*Jr? zoXKejyf4~;Y_@4}f=M@~Q&gwZx{hbTG`NaD0RuMSF77+Q^bH@Mr8=EjT4^4nA6n%i z9tlE>xZmc+N=#&ifq*b(^N3C*Yvn>Z<^GThCNXwo0#yPaEqUahHS3sQYuss607$~Cz8R1K!ytdTQxo?d8gE0OhMjg=Vlc9{cD!PJX2UTs0V zuU*%^HJ)ujJO_i;$aF@aM)6|{;%m196Ak@PoA|N?@fA$A@2@>;{Mv%}-XBwg*7&vs z@f}R9Srfz-B+!nk8nPykEl8kXob_(7zkrAn)D|T09=!BZPx>i0185+yElA*CaP<=G z=vt#<3!=7bbLErM5w%9i7DOqSs9K|H3!)SZ5XzXO+OTn2ZNZhrqmWODzpS+%omZ z(H6wh^?X%GCI;}*lyM-W+=fC*v>D?3A$y?v@M8P zV~`-$7`6p59*kdW!mtG~Zw%tcnhF$Sq_P1QF-=p?jYs#)XZ zjSxBs&lsev7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/refresh.xml b/app/src/main/res/drawable/refresh.xml new file mode 100644 index 00000000..3148e410 --- /dev/null +++ b/app/src/main/res/drawable/refresh.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/search_history_background.xml b/app/src/main/res/drawable/search_history_background.xml new file mode 100644 index 00000000..e107d40e --- /dev/null +++ b/app/src/main/res/drawable/search_history_background.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/xmark.xml b/app/src/main/res/drawable/xmark.xml new file mode 100644 index 00000000..f5e3a847 --- /dev/null +++ b/app/src/main/res/drawable/xmark.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/layout/activity_error.xml b/app/src/main/res/layout/activity_error.xml new file mode 100644 index 00000000..049a60de --- /dev/null +++ b/app/src/main/res/layout/activity_error.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..0a963658 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,6 @@ - - + + + + + + + + + android:background="#FFFFFF" + app:behavior_hideable="false" + app:behavior_peekHeight="170dp" + android:visibility="gone" + android:padding="10dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" + > + + + + - + + 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..f21556e0 --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + \ No newline at end of file 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..c580d2db --- /dev/null +++ b/app/src/main/res/layout/place_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_history_item.xml b/app/src/main/res/layout/search_history_item.xml new file mode 100644 index 00000000..559b5b4e --- /dev/null +++ b/app/src/main/res/layout/search_history_item.xml @@ -0,0 +1,29 @@ + + + + + + + + \ 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..a415949e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ Map + 검색어를 입력해주세요. + 검색 결과가 없습니다. + 오류가 발생했습니다 \n 다시 시도해 주세요. \ No newline at end of file diff --git a/app/src/test/java/campus/tech/kakao/map/MainViewModelTest.kt b/app/src/test/java/campus/tech/kakao/map/MainViewModelTest.kt new file mode 100644 index 00000000..2740b56f --- /dev/null +++ b/app/src/test/java/campus/tech/kakao/map/MainViewModelTest.kt @@ -0,0 +1,99 @@ +package campus.tech.kakao.map + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.kakao.vectormap.LatLng +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config@RunWith(RobolectricTestRunner::class) + +@Config(sdk = [28]) +class MainViewModelTest { + + private lateinit var viewModel: MainViewModel + private lateinit var preferenceManager: FakePreferenceManager + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + preferenceManager = FakePreferenceManager(context) + MapApplication.prefs = preferenceManager + viewModel = MainViewModel(preferenceManager) + } + @Test + fun setLocation_withCoordinates_returnsLatLng() { + // given + val latitude = 37.7749 + val longitude = -122.4194 + + // when + val result = viewModel.setLocation(latitude, longitude) + + // then + val expected = LatLng.from(latitude, longitude) + assertEquals(expected.latitude, result?.latitude) + assertEquals(expected.longitude, result?.longitude) + } + + @Test + fun setLocation_withoutCoordinates_returnsLastKnownLatLng() { + // given + val document = Document( + addressName = "123 Main St", + categoryGroupCode = "ABC", + categoryGroupName = "Category Group", + categoryName = "Category", + distance = "10", + id = "1", + phone = "123-456-7890", + placeName = "Test Place", + placeUrl = "http://example.com", + roadAddressName = "456 Road St", + x = "-122.4194", + y = "37.7749" + ) + val searchHistory = SearchHistory("Some history", document) + preferenceManager.addSearchHistory(searchHistory) + + // when + val result = viewModel.setLocation() + + // then + val expected = LatLng.from(37.7749, -122.4194) + assertEquals(expected.latitude, result?.latitude) + assertEquals(expected.longitude, result?.longitude) + } + + + // FakePreferenceManager 클래스 추가 + class FakePreferenceManager(context: Context) : PreferenceManager(context) { + private val searchHistory = mutableListOf() + + override fun getArrayList(key: String): ArrayList { + return ArrayList(searchHistory) + } + + override fun savePreference(key: String, searchHistory: SearchHistory, currentList: ArrayList) { + this.searchHistory.add(searchHistory) + } + + override fun deleteArrayListItem(key: String, index: Int) { + if (index >= 0 && index < searchHistory.size) { + searchHistory.removeAt(index) + } + } + + fun addSearchHistory(searchHistory: SearchHistory) { + this.searchHistory.add(searchHistory) + } + + fun clearSearchHistory() { + this.searchHistory.clear() + } + } +} + + diff --git a/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt b/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt new file mode 100644 index 00000000..6ce6d841 --- /dev/null +++ b/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt @@ -0,0 +1,182 @@ +//package campus.tech.kakao.map +// +//import android.content.Context +//import androidx.arch.core.executor.testing.InstantTaskExecutorRule +//import androidx.lifecycle.Observer +//import androidx.test.core.app.ApplicationProvider +//import androidx.test.ext.junit.runners.AndroidJUnit4 +//import kotlinx.coroutines.test.UnconfinedTestDispatcher +//import kotlinx.coroutines.test.advanceUntilIdle +//import kotlinx.coroutines.test.runTest +//import org.junit.* +//import org.junit.Assert.* +//import org.junit.runner.RunWith +//import org.robolectric.annotation.Config +// +//@RunWith(AndroidJUnit4::class) +//@Config(manifest = Config.NONE) +//class SearchViewModelTest { +// +// @get:Rule +// val instantTaskExecutorRule = InstantTaskExecutorRule() +// +// private lateinit var viewModel: SearchViewModel +// private lateinit var context: Context +// private lateinit var preferenceManager: FakePreferenceManager +// private lateinit var repository: FakeRetrofitRepository +// +// @Before +// fun setUp() { +// context = ApplicationProvider.getApplicationContext() +// preferenceManager = FakePreferenceManager(context) +// repository = FakeRetrofitRepository() +// MapApplication.prefs = preferenceManager +// +// viewModel = SearchViewModel(context) +// viewModel.repository = repository // repository를 설정합니다. +// } +// +// @Test +// fun getSearchHistoryList_setsSearchHistory() { +// val document = Document( +// addressName = "Address1", +// categoryGroupCode = "CategoryGroupCode1", +// categoryGroupName = "CategoryGroupName1", +// categoryName = "CategoryName1", +// distance = "Distance1", +// id = "Id1", +// phone = "Phone1", +// placeName = "Place1", +// placeUrl = "PlaceUrl1", +// roadAddressName = "RoadAddressName1", +// x = "X1", +// y = "Y1" +// ) +// val searchHistory = SearchHistory("Search1", document) +// preferenceManager.addSearchHistory(searchHistory) +// +// val observer = Observer> {} +// viewModel.searchHistoryList.observeForever(observer) +// +// viewModel.getSearchHistoryList() +// assertEquals(1, viewModel.searchHistoryList.value!!.size) +// assertEquals("Search1", viewModel.searchHistoryList.value!![0].searchHistory) +// } +// +// @Test +// fun saveSearchHistory_updatesSearchHistory() { +// val document = Document( +// addressName = "Address1", +// categoryGroupCode = "CategoryGroupCode1", +// categoryGroupName = "CategoryGroupName1", +// categoryName = "CategoryName1", +// distance = "Distance1", +// id = "Id1", +// phone = "Phone1", +// placeName = "Place1", +// placeUrl = "PlaceUrl1", +// roadAddressName = "RoadAddressName1", +// x = "X1", +// y = "Y1" +// ) +// val searchHistory = SearchHistory("Search1", document) +// +// viewModel.saveSearchHistory(searchHistory) +// +// assertEquals(1, preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY).size) +// assertEquals("Search1", preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY)[0].searchHistory) +// } +// +// @Test +// fun deleteSearchHistory_removesItemFromSearchHistory() { +// val document = Document( +// addressName = "Address1", +// categoryGroupCode = "CategoryGroupCode1", +// categoryGroupName = "CategoryGroupName1", +// categoryName = "CategoryName1", +// distance = "Distance1", +// id = "Id1", +// phone = "Phone1", +// placeName = "Place1", +// placeUrl = "PlaceUrl1", +// roadAddressName = "RoadAddressName1", +// x = "X1", +// y = "Y1" +// ) +// val searchHistory = SearchHistory("Search1", document) +// preferenceManager.addSearchHistory(searchHistory) +// +// viewModel.deleteSearchHistory(0) +// +// assertTrue(preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY).isEmpty()) +// } +// +// @Test +// fun getPlace_setsLocationList() = runTest(UnconfinedTestDispatcher()) { +// val searchText = "Place" +// val documents = listOf( +// Document( +// addressName = "Address1", +// categoryGroupCode = "CategoryGroupCode1", +// categoryGroupName = "CategoryGroupName1", +// categoryName = "CategoryName1", +// distance = "Distance1", +// id = "Id1", +// phone = "Phone1", +// placeName = "Place1", +// placeUrl = "PlaceUrl1", +// roadAddressName = "RoadAddressName1", +// x = "X1", +// y = "Y1" +// ) +// ) +// repository.setPlaces(documents) +// +// val observer = Observer> {} +// viewModel.locationList.observeForever(observer) +// +// viewModel.getPlace(searchText) +// advanceUntilIdle() +// +// assertEquals(1, viewModel.locationList.value!!.size) +// assertEquals("Place1", viewModel.locationList.value!![0].placeName) +// } +//} +// +//class FakePreferenceManager(context: Context) : PreferenceManager(context) { +// private val searchHistory = mutableListOf() +// +// override fun getArrayList(key: String): ArrayList { +// return ArrayList(searchHistory) +// } +// +// override fun savePreference(key: String, searchHistory: SearchHistory, currentList: ArrayList) { +// this.searchHistory.add(searchHistory) +// } +// +// override fun deleteArrayListItem(key: String, index: Int) { +// if (index >= 0 && index < searchHistory.size) { +// searchHistory.removeAt(index) +// } +// } +// +// fun addSearchHistory(searchHistory: SearchHistory) { +// this.searchHistory.add(searchHistory) +// } +// +// fun clearSearchHistory() { +// this.searchHistory.clear() +// } +//} +// +//class FakeRetrofitRepository : RetrofitRepository() { +// private var places = listOf() +// +// fun setPlaces(places: List) { +// this.places = places +// } +// +// override suspend fun getPlace(query: String): List { +// return places +// } +//} diff --git a/settings.gradle.kts b/settings.gradle.kts index 48bba0fc..c0c977eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ dependencyResolutionManagement { google() mavenCentral() maven("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") + maven("https://devrepo.kakao.com/nexus/content/groups/public/") } } From ddb4e595aaa04ce1e78dfdd8bd2db1d5560f3ab3 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 25 Jul 2024 15:27:57 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(build.gradle):=20Hilt=20=EC=A2=85?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b1e726da..6535810e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("org.jlleitschuh.gradle.ktlint") id("kotlin-parcelize") id("kotlin-kapt") - id("com.google.dagger.hilt.android") + id("dagger.hilt.android.plugin") } android { From 94972b150005019056aa7122d1262c9a21950401 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 25 Jul 2024 15:29:33 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor(Map):=20Hilt=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../campus/tech/kakao/map/MainActivity.kt | 9 ++-- .../campus/tech/kakao/map/MainViewModel.kt | 9 +++- .../campus/tech/kakao/map/MapAppModule.kt | 26 ++++++++++++ .../campus/tech/kakao/map/MapApplication.kt | 10 +++-- .../campus/tech/kakao/map/RepositoryModule.kt | 41 +++++++++++++++++++ .../tech/kakao/map/RetrofitRepository.kt | 5 ++- .../campus/tech/kakao/map/SearchActivity.kt | 8 ++-- .../campus/tech/kakao/map/SearchViewModel.kt | 13 ++++-- .../campus/tech/kakao/map/ViewModelFactory.kt | 8 ++-- 9 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/campus/tech/kakao/map/MapAppModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/RepositoryModule.kt 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 8e854543..e912ef2f 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -16,14 +16,17 @@ import com.kakao.vectormap.label.Label import com.kakao.vectormap.label.LabelOptions import com.kakao.vectormap.label.LabelStyle import com.kakao.vectormap.label.LabelStyles +import dagger.hilt.android.AndroidEntryPoint import okhttp3.Address +import javax.inject.Inject +@AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var mainViewBinding: ActivityMainBinding private lateinit var mapView: MapView - private val mainViewModel: MainViewModel by viewModels { - ViewModelFactory(applicationContext, MapApplication.prefs) - } + @Inject lateinit var preferenceManager: PreferenceManager + + private val mainViewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt b/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt index 02f93898..42a2b57c 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainViewModel.kt @@ -1,10 +1,15 @@ package campus.tech.kakao.map +import android.content.Context import androidx.lifecycle.ViewModel import com.kakao.vectormap.LatLng +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject -class MainViewModel( - private val preferenceManager: PreferenceManager +@HiltViewModel +class MainViewModel @Inject constructor( + private val context: Context, + private val preferenceManager: PreferenceManager, ) : ViewModel() { fun setLocation(latitude: Double? = null, longitude: Double? = null): LatLng? { diff --git a/app/src/main/java/campus/tech/kakao/map/MapAppModule.kt b/app/src/main/java/campus/tech/kakao/map/MapAppModule.kt new file mode 100644 index 00000000..7d7b58c9 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MapAppModule.kt @@ -0,0 +1,26 @@ +package campus.tech.kakao.map + +import android.app.Application +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun providePreferenceManager(context: Context): PreferenceManager { + return PreferenceManager(context) + } + + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application.applicationContext + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MapApplication.kt b/app/src/main/java/campus/tech/kakao/map/MapApplication.kt index ef5bbabd..a7867e5f 100644 --- a/app/src/main/java/campus/tech/kakao/map/MapApplication.kt +++ b/app/src/main/java/campus/tech/kakao/map/MapApplication.kt @@ -4,15 +4,17 @@ import android.app.Application import android.content.SharedPreferences import android.util.Log import com.kakao.vectormap.KakaoMapSdk +import dagger.hilt.android.HiltAndroidApp +@HiltAndroidApp class MapApplication: Application() { - companion object { - lateinit var prefs: PreferenceManager - } +// companion object { +// lateinit var prefs: PreferenceManager +// } override fun onCreate() { val apiKey = getString(R.string.kakao_api_key) - prefs = PreferenceManager(applicationContext) +// prefs = PreferenceManager(applicationContext) super.onCreate() KakaoMapSdk.init(this, apiKey) } diff --git a/app/src/main/java/campus/tech/kakao/map/RepositoryModule.kt b/app/src/main/java/campus/tech/kakao/map/RepositoryModule.kt new file mode 100644 index 00000000..024808ea --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/RepositoryModule.kt @@ -0,0 +1,41 @@ +package campus.tech.kakao.map + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RepositoryModule { + + @Module + @InstallIn(SingletonComponent::class) + object NetworkModule { + + @Provides + @Singleton + fun provideRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl("https://dapi.kakao.com/v2/local/search/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + @Provides + @Singleton + fun provideRetrofitService(retrofit: Retrofit): RetrofitService { + return retrofit.create(RetrofitService::class.java) + } + + @Provides + @Singleton + fun provideRetrofitRepository(retrofitService: RetrofitService): RetrofitRepository { + return RetrofitRepository(retrofitService) + } + } + +} diff --git a/app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt b/app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt index 9981706a..618951ad 100644 --- a/app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/RetrofitRepository.kt @@ -5,8 +5,11 @@ import campus.tech.kakao.map.RetrofitInstance.retrofitService import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import javax.inject.Inject -open class RetrofitRepository { +open class RetrofitRepository @Inject constructor( + private val retrofitService: RetrofitService +) { open suspend fun getPlace(query: String): List { if (query.isEmpty()) { return emptyList() diff --git a/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt index 475b5e29..b395a9f3 100644 --- a/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/SearchActivity.kt @@ -13,16 +13,18 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import campus.tech.kakao.map.databinding.ActivitySearchBinding +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import javax.inject.Inject +@AndroidEntryPoint class SearchActivity : AppCompatActivity() { - private val viewModel: SearchViewModel by viewModels { - ViewModelFactory(applicationContext, MapApplication.prefs) - } + @Inject lateinit var preferenceManager: PreferenceManager + private val viewModel: SearchViewModel by viewModels() private val placeAdapter: PlaceAdapter by lazy { PlaceAdapter( diff --git a/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt index b5a95b7f..e7e3dee5 100644 --- a/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt @@ -6,15 +6,20 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext - -class SearchViewModel(context: Context) : ViewModel() { +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + context: Context, + private val preferenceManager: PreferenceManager, + var repository: RetrofitRepository +) : ViewModel() { private val dbHelper: DBHelper = DBHelper(context) private val db = dbHelper.writableDatabase - private val preferenceManager = MapApplication.prefs - var repository = RetrofitRepository() private var _placeList = MutableLiveData>() private val _searchHistoryList = MutableLiveData>() diff --git a/app/src/main/java/campus/tech/kakao/map/ViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/ViewModelFactory.kt index 6cd90a41..348758bb 100644 --- a/app/src/main/java/campus/tech/kakao/map/ViewModelFactory.kt +++ b/app/src/main/java/campus/tech/kakao/map/ViewModelFactory.kt @@ -5,15 +5,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider class ViewModelFactory(private val context: Context - ,private val preferenceManager: PreferenceManager) + ,private val preferenceManager: PreferenceManager + , private val repository: RetrofitRepository +) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(SearchViewModel::class.java)) { - return SearchViewModel(context) as T + return SearchViewModel(context, preferenceManager, repository) as T } else if (modelClass.isAssignableFrom(MainViewModel::class.java)) { - return MainViewModel(preferenceManager) as T + return MainViewModel(context, preferenceManager) as T } else { throw IllegalArgumentException("Failed to create ViewModel : ${modelClass.name}")