From 0d09eaa75e44087eee15c3f61c2f889cd91e7633 Mon Sep 17 00:00:00 2001 From: fiveJinw Date: Wed, 24 Jul 2024 19:29:46 +0900 Subject: [PATCH 01/20] init : Copy week 4 code --- .gitignore | 7 +- app/build.gradle.kts | 53 ++-- .../tech/kakao/map/view/MapActivityTest.kt | 69 +++++ .../tech/kakao/map/view/SearchActivityTest.kt | 202 +++++++++++++ app/src/main/AndroidManifest.xml | 12 +- .../campus/tech/kakao/map/MainApplication.kt | 11 + .../kakao/map/adapter/PlaceViewAdapter.kt | 72 +++++ .../map/adapter/SavedPlaceViewAdapter.kt | 78 +++++ .../campus/tech/kakao/map/db/PlaceContract.kt | 20 ++ .../campus/tech/kakao/map/db/PlaceDBHelper.kt | 94 ++++++ .../campus/tech/kakao/map/model/Constants.kt | 11 + .../java/campus/tech/kakao/map/model/Place.kt | 17 ++ .../campus/tech/kakao/map/model/SavedPlace.kt | 5 + .../map/repository/KakaoApiDataSource.kt | 53 ++++ .../kakao/map/repository/KakaoLocalApi.kt | 16 + .../kakao/map/repository/PlaceRepository.kt | 70 +++++ .../map/repository/SavedPlaceRepository.kt | 43 +++ .../repository/SharedPreferenceRepository.kt | 30 ++ .../tech/kakao/map/view/ClickListener.kt | 13 + .../campus/tech/kakao/map/view/MapActivity.kt | 229 ++++++++++++++ .../tech/kakao/map/view/SearchActivity.kt | 178 +++++++++++ .../map/viewmodel/MapActivityViewModel.kt | 41 +++ .../map/viewmodel/MapViewModelFactory.kt | 16 + .../map/viewmodel/SearchActivityViewModel.kt | 54 ++++ .../map/viewmodel/SearchViewModelFactory.kt | 18 ++ .../main/res/drawable/ic_location_marker.png | Bin 0 -> 1032 bytes .../res/drawable/ic_location_marker_2.png | Bin 0 -> 1705 bytes app/src/main/res/layout/activity_map.xml | 65 ++++ app/src/main/res/layout/activity_search.xml | 89 ++++++ app/src/main/res/layout/bottom_sheet.xml | 40 +++ app/src/main/res/layout/place_item.xml | 60 ++++ app/src/main/res/layout/saved_place_item.xml | 30 ++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 1 + .../map/repository/PlaceRepositoryTest.kt | 41 +++ .../repository/SavedPlaceRepositoryTest.kt | 79 +++++ .../viewmodel/SearchActivityViewModelTest.kt | 84 ++++++ build.gradle.kts | 1 - gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 282 +++++++++++------- gradlew.bat | 15 +- settings.gradle.kts | 12 +- 44 files changed, 2058 insertions(+), 159 deletions(-) create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/view/MapActivityTest.kt create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/view/SearchActivityTest.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/MainApplication.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/adapter/PlaceViewAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/adapter/SavedPlaceViewAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/db/PlaceContract.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/db/PlaceDBHelper.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/Constants.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/Place.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/SavedPlace.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/KakaoApiDataSource.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/KakaoLocalApi.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/PlaceRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/SavedPlaceRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/SharedPreferenceRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/ClickListener.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/MapActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/MapActivityViewModel.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/SearchActivityViewModel.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/SearchViewModelFactory.kt create mode 100644 app/src/main/res/drawable/ic_location_marker.png create mode 100644 app/src/main/res/drawable/ic_location_marker_2.png create mode 100644 app/src/main/res/layout/activity_map.xml create mode 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/layout/bottom_sheet.xml create mode 100644 app/src/main/res/layout/place_item.xml create mode 100644 app/src/main/res/layout/saved_place_item.xml create mode 100644 app/src/test/java/campus/tech/kakao/map/repository/PlaceRepositoryTest.kt create mode 100644 app/src/test/java/campus/tech/kakao/map/repository/SavedPlaceRepositoryTest.kt create mode 100644 app/src/test/java/campus/tech/kakao/map/viewmodel/SearchActivityViewModelTest.kt diff --git a/.gitignore b/.gitignore index b4959436..d658d9f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -.DS_Store -### Android template # Gradle files .gradle/ build/ @@ -33,5 +31,6 @@ google-services.json # Android Profiling *.hprof -/keyStore -/app/release + +# Mac OS +.DS_Store diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c1a15f2..ee88ba45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,16 +1,17 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") - id("org.jlleitschuh.gradle.ktlint") id("kotlin-parcelize") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") } android { namespace = "campus.tech.kakao.map" compileSdk = 34 + defaultConfig { applicationId = "campus.tech.kakao.map" minSdk = 26 @@ -18,7 +19,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 { @@ -39,42 +45,35 @@ android { } buildFeatures { - dataBinding = true + viewBinding = true buildConfig = true } + } dependencies { - - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("com.google.android.material:material:1.12.0") + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") 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("androidx.activity:activity-ktx:1.9.0") - implementation("androidx.test:core-ktx:1.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") - implementation("androidx.room:room-runtime:2.6.1") - kapt("androidx.room:room-compiler:2.6.1") - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - implementation("androidx.activity:activity-ktx:1.9.0") - implementation("androidx.room:room-ktx:2.6.1") - testImplementation("androidx.room:room-testing:2.6.1") + implementation("androidx.activity:activity:1.8.0") + implementation("androidx.test:core-ktx:1.5.0") + implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") testImplementation("junit:junit:4.13.2") testImplementation("io.mockk:mockk-android:1.13.11") testImplementation("io.mockk:mockk-agent:1.13.11") testImplementation("androidx.arch.core:core-testing:2.2.0") testImplementation("org.robolectric:robolectric:4.11.1") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") - androidTestImplementation("androidx.test:rules:1.6.1") - 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") -} + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation("androidx.test:rules:1.5.0") + androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1") + androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1") + +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/view/MapActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/view/MapActivityTest.kt new file mode 100644 index 00000000..f1fc9670 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/view/MapActivityTest.kt @@ -0,0 +1,69 @@ +package campus.tech.kakao.map.view + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.idling.CountingIdlingResource +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.Visibility +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.intent.Intents + +import org.junit.Test +import campus.tech.kakao.map.R +import org.junit.After +import org.junit.Before +import org.junit.Rule + + +class MapActivityTest{ + + + @get:Rule + val activityRule = ActivityScenarioRule(MapActivity::class.java) + + + @Before + fun setUp() { + Intents.init() + testInit() + } + + @After + fun tearDown() { + Intents.release() + } + @Test + // 초기 화면이 잘 구성되는지 + fun testInit() { + onView(withId(R.id.input_search_field)).check( + matches(isDisplayed())) + onView(withId(R.id.search_icon)).check( + matches(isDisplayed())) + onView(withId(R.id.error_text)).check( + matches(withEffectiveVisibility(Visibility.GONE))) + onView(withId(R.id.map_view)).check( + matches(isDisplayed())) + } + + @Test + // 오류가 났을 때 오류화면이 잘 띄워지는지 + fun testError(){ + activityRule.scenario.onActivity { activity -> + activity.showErrorMessageView("인증 과정 중 원인을 알 수 없는 에러가 발생했습니다") + } + onView(withId(R.id.error_text)).check(matches(isDisplayed())) + } + + @Test + // 액티비티가 잘 이동하는지 + fun testMoveSearchActivity(){ + onView(withId(R.id.input_search_field)).perform(click()) + intended(hasComponent("campus.tech.kakao.map.view.SearchActivity")) + } +} diff --git a/app/src/androidTest/java/campus/tech/kakao/map/view/SearchActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/view/SearchActivityTest.kt new file mode 100644 index 00000000..9bd584d3 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/view/SearchActivityTest.kt @@ -0,0 +1,202 @@ +package campus.tech.kakao.map.view + + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.view.View +import android.widget.EditText +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.* +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.* +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.* +import androidx.test.espresso.intent.matcher.IntentMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.rules.ActivityScenarioRule +import campus.tech.kakao.map.R +import campus.tech.kakao.map.adapter.PlaceViewAdapter +import campus.tech.kakao.map.adapter.PlaceViewHolder +import campus.tech.kakao.map.adapter.SavedPlaceViewAdapter +import campus.tech.kakao.map.model.Constants +import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.model.SavedPlace +import org.hamcrest.core.AllOf.* +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.regex.Matcher + + +class SearchActivityTest { + + @get:Rule + var activityRule = ActivityScenarioRule(SearchActivity::class.java) + + @Before + fun setUp() { + Intents.init() + } + + @After + fun tearDown() { + Intents.release() + } + + @Test + // 초기 화면이 잘 구성되는지 + fun testInit() { + onView(withId(R.id.search_result_recyclerView)).check( + matches(isDisplayed()) + ) + onView(withId(R.id.input_search_field)).check( + matches(isDisplayed()) + ) + onView(withId(R.id.no_search_result)).check( + matches(isDisplayed()) + ) + onView(withId(R.id.saved_search_recyclerView)).check( + matches(isDisplayed()) + ) + } + + @Test + // PlaceRecyclerView가 검색어를 입력했을 때 최대 15개의 데이터를 불러오는 지 확인 + fun testPlaceRecyclerView() { + + onView(withId(R.id.input_search_field)).check(matches(isDisplayed())) + onView(withId(R.id.search_result_recyclerView)).check(matches(isDisplayed())) + onView(withId(R.id.input_search_field)).perform(replaceText("전남대")) + Thread.sleep(1000) + + activityRule.scenario.onActivity { activity -> + val inputSearchField = activity.findViewById(R.id.input_search_field) + val placeRecyclerView = + activity.findViewById(R.id.search_result_recyclerView) + val itemCount = placeRecyclerView.adapter?.getItemCount() ?: 0 + assert(itemCount in 1..15) + } + } + + @Test + // recyclerview의 아이템을 클릭했을 때 이동이 잘 되는지 + fun testMoveToMapActivity() { + onView(withId(R.id.input_search_field)).check(matches(isDisplayed())) + onView(withId(R.id.search_result_recyclerView)).check(matches(isDisplayed())) + onView(withId(R.id.input_search_field)).perform(replaceText("전남대")) + Thread.sleep(1000) + lateinit var place: Place + + activityRule.scenario.onActivity { activity -> + val searchRecyclerView = + activity.findViewById(R.id.search_result_recyclerView) + val adapter = searchRecyclerView.adapter as PlaceViewAdapter + place = (adapter.getPlaceAtPosition(0)) + } + + val intent = Intent() + intent.putExtra(Constants.Keys.KEY_PLACE, place) + val result = Instrumentation.ActivityResult(Activity.RESULT_OK, intent) + onView(withId(R.id.search_result_recyclerView)).perform( + actionOnItemAtPosition( + 0, + click() + ) + ) + + intending(toPackage("campus.tech.kakao.map.view.MapActivity")).respondWith(result) + } + + @Test + // 아이템 클릭시 데이터가 저장되는지 + fun testSavePlace() { + onView(withId(R.id.input_search_field)).check(matches(isDisplayed())) + onView(withId(R.id.search_result_recyclerView)).check(matches(isDisplayed())) + onView(withId(R.id.input_search_field)).perform(replaceText("전남대")) + Thread.sleep(1000) + + var existTaget: Boolean + lateinit var place: Place + activityRule.scenario.onActivity { activity -> + val searchRecyclerView = + activity.findViewById(R.id.search_result_recyclerView) + val placeViewAdapter = searchRecyclerView.adapter as PlaceViewAdapter + place = (placeViewAdapter.getPlaceAtPosition(0)) + } + + onView(withId(R.id.search_result_recyclerView)).perform( + actionOnItemAtPosition(0,click()) + ) + + val newScenario = ActivityScenario.launch(SearchActivity::class.java) + + newScenario.onActivity { activity -> + val savedPlaceRecyclerView = + activity.findViewById(R.id.saved_search_recyclerView) + val savedPlaceAdapter = savedPlaceRecyclerView.adapter as SavedPlaceViewAdapter + existTaget = savedPlaceAdapter.existPlace(SavedPlace(place.name)) + assertTrue(existTaget) + } + } + + @Test + // 삭제 버튼 클릭시 저장된 데이터가 삭제되는지 + fun testRemoveSavedPlace() { + onView(withId(R.id.input_search_field)).check(matches(isDisplayed())) + onView(withId(R.id.search_result_recyclerView)).check(matches(isDisplayed())) + Thread.sleep(1000) + + var existTaget: Boolean + lateinit var savedPlace: SavedPlace + lateinit var savedPlaceRecyclerView : RecyclerView + lateinit var savedPlaceAdapter : SavedPlaceViewAdapter + + activityRule.scenario.onActivity { activity -> + savedPlaceRecyclerView = + activity.findViewById(R.id.saved_search_recyclerView) + savedPlaceAdapter = savedPlaceRecyclerView.adapter as SavedPlaceViewAdapter + + savedPlace = (savedPlaceAdapter.getSavedPlaceAtPosition(0)) + existTaget = savedPlaceAdapter.existPlace(savedPlace) + assertTrue(existTaget) + } + + onView(withId(R.id.saved_search_recyclerView)).perform( + actionOnItemAtPosition(0, MyViewAction.clickChildViewWithId(R.id.button_saved_delete)) + ) + Thread.sleep(1000) + + activityRule.scenario.onActivity { activity -> + existTaget = savedPlaceAdapter.existPlace(savedPlace) + assertFalse(existTaget) + } + + + } + object MyViewAction { + fun clickChildViewWithId(id: Int): ViewAction { + return object : ViewAction { + override fun getConstraints(): org.hamcrest.Matcher { + return isAssignableFrom(View::class.java) + } + + override fun getDescription(): String { + return "Click on a child view with specified id." + } + + override fun perform(uiController: UiController?, view: View) { + val v = view.findViewById(id) + v.performClick() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..d2438a55 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + @@ -21,6 +28,7 @@ + - + \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MainApplication.kt b/app/src/main/java/campus/tech/kakao/map/MainApplication.kt new file mode 100644 index 00000000..8e63d8da --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MainApplication.kt @@ -0,0 +1,11 @@ +package campus.tech.kakao.map + +import android.app.Application +import com.kakao.vectormap.KakaoMapSdk + +class MainApplication : Application() { + override fun onCreate() { + super.onCreate() + KakaoMapSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..062963fb --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/PlaceViewAdapter.kt @@ -0,0 +1,72 @@ +package campus.tech.kakao.map.adapter + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.view.OnClickPlaceListener +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.Place + +class PlaceViewAdapter( + val listener: OnClickPlaceListener +) : ListAdapter(PlaceDiffCallBack()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.place_item, parent, false) + Log.d("testt", "검색 결과 뷰 생성") + return PlaceViewHolder(view, listener) + } + + override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) { +// Log.d("testt", "${placeList?.get(position)?.name}, ${placeList?.get(position)?.location}, ${placeList?.get(position)?.category}") + val currentPlace = getItem(position) + holder.bind(currentPlace) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun getPlaceAtPosition(position : Int) : Place{ + return getItem(position) + } +} + +class PlaceDiffCallBack : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Place, newItem: Place): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: Place, newItem: Place): Boolean { + return oldItem == newItem + } +} + +class PlaceViewHolder(itemView: View, val listener: OnClickPlaceListener) : + RecyclerView.ViewHolder(itemView) { + val name = itemView.findViewById(R.id.place_name) + val location = itemView.findViewById(R.id.place_location) + val category = itemView.findViewById(R.id.place_category) + var currentPlace : Place? = null + + init { + itemView.setOnClickListener { + val position = absoluteAdapterPosition + Log.d("testt", "콜백함수 호출") + currentPlace?.let { listener.savePlace(it) } + } + } + + fun bind(place : Place){ + currentPlace = place + name.text = place.name + 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 new file mode 100644 index 00000000..c609b84b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/SavedPlaceViewAdapter.kt @@ -0,0 +1,78 @@ +package campus.tech.kakao.map.adapter + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.view.OnClickSavedPlaceListener +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.model.SavedPlace + +class SavedPlaceViewAdapter( + val listener: OnClickSavedPlaceListener +) : ListAdapter(SavedPlaceDiffCallBack()) { + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedPlaceViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.saved_place_item, parent, false) + Log.d("testt", "저장된 장소를 띄우는 뷰 홀더 생성") + return SavedPlaceViewHolder(view, listener) + } + + override fun onBindViewHolder(holder: SavedPlaceViewHolder, position: Int) { + val currentSavedPlace = getItem(position) + holder.bind(currentSavedPlace) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun existPlace(savedPlace: SavedPlace) : Boolean = currentList.contains(savedPlace) + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun getSavedPlaceAtPosition(position : Int) : SavedPlace{ + return getItem(position) + } +} + +class SavedPlaceDiffCallBack : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SavedPlace, newItem: SavedPlace): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: SavedPlace, newItem: SavedPlace): Boolean { + return oldItem.name == newItem.name + } +} + +class SavedPlaceViewHolder(itemView: View, val listener: OnClickSavedPlaceListener) : + RecyclerView.ViewHolder(itemView) { + val name = itemView.findViewById(R.id.saved_place_name) + val deleteButton = itemView.findViewById(R.id.button_saved_delete) + var currentSavedPlace: SavedPlace? = null + + init { + deleteButton.setOnClickListener { + val position = absoluteAdapterPosition + Log.d("testt", "삭제 콜백함수 호출") + currentSavedPlace?.let { listener.deleteSavedPlace(it, position) } + } + itemView.setOnClickListener { + currentSavedPlace?.let { listener.loadPlace(it) } + } + + } + + fun bind(savedPlace: SavedPlace) { + currentSavedPlace = savedPlace + name.text = savedPlace.name + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/db/PlaceContract.kt b/app/src/main/java/campus/tech/kakao/map/db/PlaceContract.kt new file mode 100644 index 00000000..3a4ede89 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/db/PlaceContract.kt @@ -0,0 +1,20 @@ +package campus.tech.kakao.map.db + +import android.provider.BaseColumns + +object PlaceContract{ + const val DATABASE_NAME = "PLACE.DB" + const val VERSION = 2 + + object PlaceEntry : BaseColumns { + const val TABLE_NAME = "PLACE" + const val COLUMN_NAME = "NAME" + const val COLUMN_LOCATION = "LOCATION" + const val COLUMN_CATEGORY = "CATEGORY" + } + + object SavedPlaceEntry : BaseColumns{ + const val TABLE_NAME = "SAVED_PLACE" + const val COLUMN_NAME = "SAVED_NAME" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/db/PlaceDBHelper.kt b/app/src/main/java/campus/tech/kakao/map/db/PlaceDBHelper.kt new file mode 100644 index 00000000..d400ab59 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/db/PlaceDBHelper.kt @@ -0,0 +1,94 @@ +package campus.tech.kakao.map.db + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log + + +class PlaceDBHelper(context: Context) : SQLiteOpenHelper(context, PlaceContract.DATABASE_NAME, null, PlaceContract.VERSION) { + override fun onCreate(db: SQLiteDatabase) { + createLocalTables(db) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("DROP TABLE IF EXISTS ${PlaceContract.PlaceEntry.TABLE_NAME};") + db.execSQL("DROP TABLE IF EXISTS ${PlaceContract.SavedPlaceEntry.TABLE_NAME};") + createLocalTables(db) + } + + fun createLocalTables(db: SQLiteDatabase){ + createPlaceTable(db) + createSavedPlaceTable(db) + } + + fun createPlaceTable(db: SQLiteDatabase){ + db.execSQL( + "CREATE TABLE " + + "${PlaceContract.PlaceEntry.TABLE_NAME}( " + + " ${PlaceContract.PlaceEntry.COLUMN_NAME} varchar(60) not null ," + + " ${PlaceContract.PlaceEntry.COLUMN_LOCATION} varchar(255) not null, " + + " ${PlaceContract.PlaceEntry.COLUMN_CATEGORY} varchar(30) );" + ) + } + + fun createSavedPlaceTable(db: SQLiteDatabase){ + db.execSQL( + "CREATE TABLE " + + "${PlaceContract.SavedPlaceEntry.TABLE_NAME}( " + + " ${PlaceContract.SavedPlaceEntry.COLUMN_NAME} varchar(60) not null);" + ) + } + + fun insertPlaceData(name: String, location: String, category: String) { + val db = this.writableDatabase + val values = ContentValues().apply { + put(PlaceContract.PlaceEntry.COLUMN_NAME, name) + put(PlaceContract.PlaceEntry.COLUMN_LOCATION, location) + put(PlaceContract.PlaceEntry.COLUMN_CATEGORY, category) + } + db.insert(PlaceContract.PlaceEntry.TABLE_NAME, null, values) + } + + fun insertSavedPlaceData(name: String) { + val db = this.writableDatabase + val values = ContentValues().apply { + put(PlaceContract.SavedPlaceEntry.COLUMN_NAME, name) + } + db.insert(PlaceContract.SavedPlaceEntry.TABLE_NAME, null, values) + } + + fun readPlaceData(): Cursor { + val rDb = this.readableDatabase + return rDb.rawQuery("SELECT * FROM ${PlaceContract.PlaceEntry.TABLE_NAME};", null) + } + + fun readPlaceDataWithSamedCategory(category: String): Cursor { + val rDb = this.readableDatabase + return rDb.rawQuery( + "SELECT * FROM ${PlaceContract.PlaceEntry.TABLE_NAME} WHERE ${PlaceContract.PlaceEntry.COLUMN_CATEGORY} = '${category}';", + null + ) + } + + fun readSavedPlaceData(): Cursor { + val rDb = this.readableDatabase + return rDb.rawQuery("SELECT * FROM ${PlaceContract.SavedPlaceEntry.TABLE_NAME};", null) + } + + fun readSavedPlaceDataWithSamedName(name: String): Cursor { + val rDb = this.readableDatabase + return rDb.rawQuery( + "SELECT * FROM ${PlaceContract.SavedPlaceEntry.TABLE_NAME} WHERE ${PlaceContract.SavedPlaceEntry.COLUMN_NAME} = '${name}';", + null + ) + } + + fun deleteSavedPlace(name: String) { + val db = this.writableDatabase + db.delete(PlaceContract.SavedPlaceEntry.TABLE_NAME, "${PlaceContract.SavedPlaceEntry.COLUMN_NAME} = ?", arrayOf(name)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/Constants.kt b/app/src/main/java/campus/tech/kakao/map/model/Constants.kt new file mode 100644 index 00000000..264ed0c0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/Constants.kt @@ -0,0 +1,11 @@ +package campus.tech.kakao.map.model + +class Constants { + object Keys{ + const val KEY_PLACE = "place" + } + + object DataStore{ + const val PREFERENCES_NAME = "location_preferences" + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..0e074aac --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/Place.kt @@ -0,0 +1,17 @@ +package campus.tech.kakao.map.model + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +data class ResultSearch( + val documents: List +) +@Parcelize +data class Place( + @SerializedName("place_name") val name : String, + @SerializedName("road_address_name") val location : String?, + @SerializedName("category_group_name") val category : String?, + @SerializedName("x") val x: String? = "", + @SerializedName("y") val y: String? = "" +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/SavedPlace.kt b/app/src/main/java/campus/tech/kakao/map/model/SavedPlace.kt new file mode 100644 index 00000000..be8f2799 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/SavedPlace.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.model + +data class SavedPlace( + val name: String +) \ No newline at end of file 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 new file mode 100644 index 00000000..d332b2a4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/PlaceRepository.kt @@ -0,0 +1,70 @@ +package campus.tech.kakao.map.repository + +import android.util.Log +import campus.tech.kakao.map.db.PlaceContract +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() + + while (cursor.moveToNext()) { + val place = Place( + cursor.getString( + cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_NAME) + ), + cursor.getString( + cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_LOCATION) + ), + cursor.getString( + cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_CATEGORY) + ) + ) + Log.d("readData", "이름 = ${place.name}, 위치 = ${place.location}, 분류 = ${place.category}") + placeList.add(place) + } + + cursor.close() + return placeList.toList() + } + + fun writePlace(place: Place){ + val name = place.name + val location = place.location ?: "" + val category = place.category ?: "" + dbHelper.insertPlaceData(name, location, category) + } + + fun getPlaceWithCategory(category : String): List{ + val cursor = dbHelper.readPlaceDataWithSamedCategory(category) + val placeList = mutableListOf() + + while (cursor.moveToNext()) { + val place = Place( + cursor.getString( + cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_NAME) + ), + cursor.getString( + cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_LOCATION) + ), + cursor.getString( + cursor.getColumnIndexOrThrow(PlaceContract.PlaceEntry.COLUMN_CATEGORY) + ) + ) + Log.d("readData", "이름 = ${place.name}, 위치 = ${place.location}, 분류 = ${place.category}") + placeList.add(place) + } + + + 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/repository/SavedPlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SavedPlaceRepository.kt new file mode 100644 index 00000000..28edcbf4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/SavedPlaceRepository.kt @@ -0,0 +1,43 @@ +package campus.tech.kakao.map.repository + +import android.util.Log +import campus.tech.kakao.map.db.PlaceContract +import campus.tech.kakao.map.db.PlaceDBHelper +import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.model.SavedPlace + +class SavedPlaceRepository (val dbHelper: PlaceDBHelper){ + fun getAllSavedPlace() : List{ + val cursor = dbHelper.readSavedPlaceData() + val placeList = mutableListOf() + + while (cursor.moveToNext()) { + val place = SavedPlace( + cursor.getString( + cursor.getColumnIndexOrThrow(PlaceContract.SavedPlaceEntry.COLUMN_NAME) + ) + ) + Log.d("readData", "이름 = ${place.name}") + placeList.add(place) + } + + cursor.close() + return placeList + } + + fun writePlace(place: Place){ + val cursor = dbHelper.readSavedPlaceDataWithSamedName(place.name) + if (cursor.moveToFirst()) { + Log.d("testt", "데이터 중복") + // 입력의 시간순대로 정렬되기 떄문에 레코드 삭제후 다시 집어넣기 + dbHelper.deleteSavedPlace(place.name) + dbHelper.insertSavedPlaceData(place.name) + } else { + dbHelper.insertSavedPlaceData(place.name) + } + } + + fun deleteSavedPlace(savedPlace: SavedPlace){ + dbHelper.deleteSavedPlace(savedPlace.name) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/SharedPreferenceRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SharedPreferenceRepository.kt new file mode 100644 index 00000000..fc374c3e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/SharedPreferenceRepository.kt @@ -0,0 +1,30 @@ +package campus.tech.kakao.map.repository + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.doublePreferencesKey +import androidx.datastore.preferences.core.edit +import campus.tech.kakao.map.view.MapActivity +import com.kakao.vectormap.LatLng +import kotlinx.coroutines.flow.map + +class SharedPreferenceRepository(private val dataStore: DataStore) { + + companion object { + val KEY_LATITUDE = doublePreferencesKey("latitude") + val KEY_LONGITUDE = doublePreferencesKey("longitude") + } + + suspend fun putPos(latitude : Double, longitude : Double){ + dataStore.edit { preferences -> + preferences[KEY_LATITUDE] = latitude + preferences[KEY_LONGITUDE] = longitude + } + } + + val pos = dataStore.data.map { preferences -> + val latitude = preferences[KEY_LATITUDE] ?: MapActivity.LATITUDE.toDouble() + val longitude = preferences[KEY_LONGITUDE] ?: MapActivity.LONGITUDE.toDouble() + LatLng.from(latitude, longitude) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/ClickListener.kt b/app/src/main/java/campus/tech/kakao/map/view/ClickListener.kt new file mode 100644 index 00000000..47be2558 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/ClickListener.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.view + +import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.model.SavedPlace + +interface OnClickPlaceListener { + fun savePlace(place: Place) +} + +interface OnClickSavedPlaceListener { + fun deleteSavedPlace(savedPlace: SavedPlace, position: Int) + fun loadPlace(savedPlace: SavedPlace) +} \ 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..7fde9a47 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/MapActivity.kt @@ -0,0 +1,229 @@ +package campus.tech.kakao.map.view + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences.Editor +import android.os.Build +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.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.isVisible +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.Constants +import campus.tech.kakao.map.model.Place +import campus.tech.kakao.map.repository.SharedPreferenceRepository +import campus.tech.kakao.map.viewmodel.MapActivityViewModel +import campus.tech.kakao.map.viewmodel.MapViewModelFactory +import com.google.android.material.bottomsheet.BottomSheetBehavior +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.camera.CameraUpdateFactory +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles + +class MapActivity : AppCompatActivity() { + + val Context.dataStore by preferencesDataStore(name = Constants.DataStore.PREFERENCES_NAME) + + lateinit var map: MapView + lateinit var inputField: EditText + lateinit var searchIcon: ImageView + lateinit var errorTextView: TextView + lateinit var kakaoMap : KakaoMap + lateinit var resultLauncher : ActivityResultLauncher + lateinit var sharedPreferencesRepository: SharedPreferenceRepository + lateinit var bottomSheetLayout : ConstraintLayout + lateinit var placeNameField : TextView + lateinit var placeLocationField : TextView + lateinit var bottomSheetBehavior : BottomSheetBehavior + lateinit var viewModel : MapActivityViewModel + lateinit var editor : Editor + var isMapDisplay = false + + companion object ChonnamUnivLocation { + const val LATITUDE = "35.175487" + const val LONGITUDE = "126.907163" + } + + enum class ErrorCode(val code: String, val errorMessage : String){ + UNKNOWN_ERROR("-1", "인증 과정 중 원인을 알 수 없는 에러가 발생했습니다"), + CONNECTION_ERROR("-2", "통신 연결 시도 중 에러가 발생하였습니다"), + SOCKET_TIMEOUT("-3", "통신 연결 중 SocketTimeoutException 에러가 발생하였습니다"), + CONNECT_TIMEOUT("-4", "통신 시도 중 ConnectTimeoutException 에러가 발생하였습니다"), + BAD_REQUEST("400", "요청을 처리하지 못하였습니다"), + AUTHORIZED_FAILURE("401", "인증 오류가 발생하였습니다. 인증 자격 증명이 충분치 않습니다"), + FORBIDDEN("403", "권한 오류가 발생하였습니다"), + TOO_MANY_REQUESTS("429", "정해진 사용량이나, 초당 요청 한도를 초과하였습니다"), + CONNECTION_FAILURE("499", "통신이 실패하였습니다. 인터넷 연결을 확인해주십시오"), + UNKNOWN("X", "오류 코드 X"); + + companion object { + fun getErrorMessage(errorText: String): ErrorCode { + return entries.find { errorText == it.code } ?: UNKNOWN + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_map) + + + initVar() + initMapView() + initBottomSheet() + initClickListener() + initResultLauncher() + initObserver() + + + + } + + + private fun initVar() { + inputField = findViewById(R.id.input_search_field) + searchIcon = findViewById(R.id.search_icon) + errorTextView = findViewById(R.id.error_text) + placeNameField = findViewById(R.id.place_name) + placeLocationField = findViewById(R.id.place_location) + sharedPreferencesRepository = SharedPreferenceRepository(dataStore) + viewModel = ViewModelProvider( + this, MapViewModelFactory(sharedPreferencesRepository) + )[MapActivityViewModel::class.java] + bringFrontSearchField() + } + + + private fun initBottomSheet(){ + bottomSheetLayout = findViewById(R.id.bottom_sheet) + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + + 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", "string : " + error.toString()) + Log.d("testt", "message : " + error.message.toString()) + Log.d("testt", "hashCode : " + error.localizedMessage) + showErrorMessageView(error.message.toString()) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + Log.d("testt", "MapReady") + errorTextView.isVisible = false + this@MapActivity.kakaoMap = kakaoMap + isMapDisplay = true + viewModel.getRecentPos() + } + }) + } + + private fun bringFrontSearchField() { + inputField.bringToFront() + searchIcon.bringToFront() + } + + private fun initClickListener() { + inputField.setOnClickListener { + moveSearchPage(it) + } + } + + private fun initResultLauncher(){ + resultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val place = getPlaceToResult(result) + val latitude = place?.y?.toDouble() ?: LATITUDE.toDouble() + val longitude = place?.x?.toDouble()?: LONGITUDE.toDouble() + val pos = LatLng.from(latitude, longitude) + moveMapCamera(pos) + createLabel(pos) + viewModel.setRecentPos(latitude, longitude) + bottomSheetBinding(place) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + } + + fun initObserver(){ + viewModel.recentPos.observe(this, Observer { + if(isMapDisplay) moveMapCamera(it) + }) + } + + private fun moveSearchPage(view: View) { + val intent = Intent(this, SearchActivity::class.java) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + this, view, "inputFieldTransition" + ) + resultLauncher.launch(intent, options) + } + + fun showErrorMessageView(error: String) { + errorTextView.isVisible = true + val errorCode = getErrorCode(error) + val errorText = ErrorCode.getErrorMessage(errorCode).errorMessage + "\n\n" + error + errorTextView.text = errorText + } + + private fun getErrorCode(errorText: String): String { + val regex = Regex("\\((\\d+)\\)") + val code = regex.find(errorText) + Log.d("testt", "errorcode" + code?.groups?.get(1)?.value) + return code?.groups?.get(1)?.value ?: "" + } + + private fun getPlaceToResult(result: ActivityResult): Place? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + result.data?.getParcelableExtra(Constants.Keys.KEY_PLACE, Place::class.java) + } else { + result.data?.getParcelableExtra(Constants.Keys.KEY_PLACE) + } + } + + private fun moveMapCamera(pos : LatLng){ + kakaoMap.moveCamera(CameraUpdateFactory.newCenterPosition(pos)) + } + + private fun bottomSheetBinding(place: Place?){ + placeNameField.text = place?.name ?: "" + placeLocationField.text = place?.location ?: "" + } + + private fun removeAllLabel(){ + kakaoMap.labelManager?.clearAll() + } + + private fun createLabel(pos : LatLng){ + val labelManager = kakaoMap.labelManager + removeAllLabel() + val style = labelManager + ?.addLabelStyles(LabelStyles.from(LabelStyle.from(R.drawable.ic_location_marker_2).setAnchorPoint(0.5f, 1f))) + var label = kakaoMap.getLabelManager()?.getLayer()?.addLabel(LabelOptions.from("center",pos).setStyles(style).setRank(1)) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt new file mode 100644 index 00000000..f6ea1d9b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt @@ -0,0 +1,178 @@ +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 +import campus.tech.kakao.map.adapter.PlaceViewAdapter +import campus.tech.kakao.map.adapter.SavedPlaceViewAdapter +import campus.tech.kakao.map.db.PlaceDBHelper +import campus.tech.kakao.map.model.Constants +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 campus.tech.kakao.map.viewmodel.SearchActivityViewModel +import campus.tech.kakao.map.viewmodel.SearchViewModelFactory +import kotlinx.coroutines.launch + + +class SearchActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlaceListener { + lateinit var noResultText: TextView + lateinit var inputSearchField: EditText + lateinit var viewModel: SearchActivityViewModel + lateinit var savedPlaceRecyclerView: RecyclerView + lateinit var searchRecyclerView: RecyclerView + lateinit var dbHelper: PlaceDBHelper + lateinit var placeRepository: PlaceRepository + lateinit var savedPlaceRepository: SavedPlaceRepository + lateinit var searchDeleteButton: ImageView + lateinit var savedPlaceRecyclerViewAdapter: SavedPlaceViewAdapter + lateinit var searchRecyclerViewAdapter: PlaceViewAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_search) + initVar() + initListeners() + initRecyclerViews() + initObserver() + inputSearchField.requestFocus() + } + + override fun deleteSavedPlace(savedPlace: SavedPlace, position: Int) { + Log.d("testt", "삭제 콜백함수 처리") + viewModel.deleteSavedPlace(savedPlace) + } + + override fun loadPlace(savedPlace: SavedPlace){ + inputSearchField.setText(savedPlace.name) + } + + override fun savePlace(place: Place) { + Log.d("testt", "콜백함수 처리") + viewModel.savePlace(place) + intent.putExtra(Constants.Keys.KEY_PLACE, place) + Log.d("testt", "Intent" + place.toString()) + setResult(RESULT_OK, intent) + finish() + } + + fun initVar() { + noResultText = findViewById(R.id.no_search_result) + searchRecyclerView = findViewById(R.id.search_result_recyclerView) + inputSearchField = findViewById(R.id.input_search_field) + savedPlaceRecyclerView = findViewById(R.id.saved_search_recyclerView) + dbHelper = PlaceDBHelper(this) + placeRepository = PlaceRepository(dbHelper) + savedPlaceRepository = SavedPlaceRepository(dbHelper) + searchDeleteButton = findViewById(R.id.button_X) + viewModel = + ViewModelProvider( + this, + SearchViewModelFactory(placeRepository, savedPlaceRepository) + )[SearchActivityViewModel::class.java] + } + + fun initListeners() { + initDeleteButtonListener() + initInputFieldListener() + } + + fun initDeleteButtonListener() { + 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) + } + } + + fun initInputFieldListener() { + inputSearchField.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + } + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + } + + override fun afterTextChanged(searchText: Editable?) { + val text = searchText.toString() + Log.d("inputField", "text : ${text}") + Log.d("coroutine", "입력변경") + lifecycleScope.launch { + viewModel.getKakaoLocalData(text) + } + } + }) + } + + fun initRecyclerViews() { + initSearchRecyclerView() + initSavedPlaceRecyclerView() + } + + fun initSearchRecyclerView() { + searchRecyclerViewAdapter = PlaceViewAdapter(this) + searchRecyclerView.layoutManager = LinearLayoutManager(this) + searchRecyclerView.adapter = searchRecyclerViewAdapter + } + + fun initSavedPlaceRecyclerView() { + savedPlaceRecyclerViewAdapter = + SavedPlaceViewAdapter(this) + savedPlaceRecyclerView.layoutManager = + LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) + 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/MapActivityViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapActivityViewModel.kt new file mode 100644 index 00000000..7eaa6de1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapActivityViewModel.kt @@ -0,0 +1,41 @@ +package campus.tech.kakao.map.viewmodel + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 campus.tech.kakao.map.repository.SharedPreferenceRepository +import com.kakao.vectormap.LatLng +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + + +class MapActivityViewModel( + private val sharedPreferenceRepository: SharedPreferenceRepository +) : ViewModel() { + private val _recentPos = MutableLiveData() + val recentPos : LiveData get() = _recentPos + + init { + getRecentPos() + } + + fun getRecentPos(){ + viewModelScope.launch{ + sharedPreferenceRepository.pos.collectLatest { + _recentPos.value = it + } + } + } + + fun setRecentPos(latitude : Double, longitude : Double){ + viewModelScope.launch { + sharedPreferenceRepository.putPos(latitude, longitude) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt new file mode 100644 index 00000000..90c6a7b4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.repository.SharedPreferenceRepository + +class MapViewModelFactory ( + private val sharedPreferenceRepository: SharedPreferenceRepository +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MapActivityViewModel::class.java)) { + return MapActivityViewModel(sharedPreferenceRepository) as T + } + throw IllegalArgumentException("unKnown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchActivityViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchActivityViewModel.kt new file mode 100644 index 00000000..2a21b1ec --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchActivityViewModel.kt @@ -0,0 +1,54 @@ +package campus.tech.kakao.map.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +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 + + +class SearchActivityViewModel( + private val placeRepository: PlaceRepository, + private val savedPlaceRepository: SavedPlaceRepository +) : ViewModel() { + private val _place = MutableLiveData>() + private val _savedPlace = MutableLiveData>() + val place: LiveData> get() = _place + val savedPlace: LiveData> get() = _savedPlace + + init { + getSavedPlace() + } + + fun getPlace() { + _place.value = (placeRepository.getAllPlace()) + } + + fun getPlaceWithCategory(category: String) { + _place.value = (placeRepository.getPlaceWithCategory(category)) + } + + fun getSavedPlace() { + _savedPlace.value = (savedPlaceRepository.getAllSavedPlace()) + } + + fun savePlace(place: Place) { + savedPlaceRepository.writePlace(place) + getSavedPlace() + } + + fun deleteSavedPlace(savedPlace: SavedPlace) { + savedPlaceRepository.deleteSavedPlace(savedPlace) + getSavedPlace() + } + + + suspend fun getKakaoLocalData(text: String) { + if (text.isNotEmpty()) { + val placeList = placeRepository.getKakaoLocalPlaceData(text) + _place.value = (placeList) + } else _place.value = listOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchViewModelFactory.kt new file mode 100644 index 00000000..a6e7411a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchViewModelFactory.kt @@ -0,0 +1,18 @@ +package campus.tech.kakao.map.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.repository.PlaceRepository +import campus.tech.kakao.map.repository.SavedPlaceRepository + +class SearchViewModelFactory( + private val placeRepository: PlaceRepository, + private val savedPlaceRepository: SavedPlaceRepository +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(SearchActivityViewModel::class.java)) { + return SearchActivityViewModel(placeRepository, savedPlaceRepository) as T + } + throw IllegalArgumentException("unKnown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_location_marker.png b/app/src/main/res/drawable/ic_location_marker.png new file mode 100644 index 0000000000000000000000000000000000000000..13a18c86179a6af193aa2b31c83bb0713e1c5924 GIT binary patch literal 1032 zcmV+j1o!)iP)9`6KkA2fc}h?itD=8~9%|~+ zC}PE>Af}>Ho0oJ!v{h_fn7$8sNIFZV$(uC!!AFLfH}Bh*Hvw6gX${t9JLoCS$gK>c zFESdL;u`&fgx5>=2E^~l>A125Dg_g5biWYtju7luMx(kD!nK^y+f%G*AFLL}yThA= zFxR+7{-RLD5JwL&a`m&SZiI3IEhm`2Lm^}kXY|?B(de%7giO4t0b?2&%O&zvjKHCr(z-f)!_y`Nqzcbr49+N@t$}V#5IK!d5QT2vVjR} z$2ipz)Q5>rABuQwDJj;BwS)>(DRN1AskEv#T+o~501CGvdV8eN(i@zSIjNz9b>7rc z7Q#DMB4C#fH>7n~Y&;fWV-zgxolk^4Jr6?%Ll6A^8YH|H&wBCM^7Uh44|C6wpOf9{ zW+)Xn9#|!;G3GrJgT#9XERaw<^|&T4C#cURv6T~0DsZ+jwSFvQu)INd3ni9&B4zFc zq?=GGa0oNm7|UXw0vI;n5Mq5_?z;#tw)rsi=PB?g>{k7&x3nwohX3SPeKnMd=*0|4W$AT z$w3k$^-51-jSS0Bpx?ZV1`TBtOQe%BnuvH89F$R1pjSc&MUPoEvZP5PLjhOX$hgHg zk!GYz%F&TCI%~oJr%J;pv1HvojB`wEq#>m)g7hqR-n*sRZ@Q{q%v{4m!X zusLRu9(m(*XLJwNWQ$P3sj|39lu^t2k32ylup?iwIfUR;iSb-Wa9m>y8>3M!yY(66 ziydRWG-8jK94YZ0KI0C~#hF;F2HZeaBd3vxn}p*#xCI?xjd1T1w}j`6CXolYiL@4N z6pa2$7asRy(IAuqg5f>+3Xr={E@V4GJFwA66n<_xuIz?#BFpGSX&zwY0+bu6*47Q2 z(J=|u|5WVAAY)QJUaA5(EW&C+xPPdG(0OVoo8`ttyHd{?`BzNY6chk?kpJz-p2z@H z3Nl@h?OY?@S77~F2_J7|H@E0{=ZQVryjF^bFkEA+nRh(1;FNZ-K6sLL$zSMCA>Am=uD0R-_n}84XO!Q zM$YCaa0aRkx~i__jE)LVyw?MPbx@7q8fimvlgA12=X~d$d+wP#cZ-Tbz(h;@#=K6q8X9>$r;$U3Mu!cZ z&g69Z7Ye?|&*s0|gTKXV!tjkNvS~_y+`gPf&%&$eMerI3$cNJ@RT0g@GqReZ8anCp zO1hwEqyvK~nimt}8ok{bah=WuMt&h9Kqq9x2V*-Dw~6Ly3qw<@z`?PgiLaa*WHi^P zREXxg1d#4&C|(zMlfNS5VlG7<6V2NK6ssq4I=K`c`GqM#E(s5Uky7e#G{PpoC=>pG z+K>=Jema%78%DwK;_#_P*&NzZVSb~Qn!Pk?eytQ+g75LO!L1e6Z@kq6I{izNa!;t_ z&7z{KAo5?ib#i2T+&lsVJLHHf>4V>nCaXP62ig{D0KnigZXC;M#OzOOc1?V_^w*YI zI^5XG>*VQB$d4rwH$d8DSq@F46!WlJ;+y<}9HAwaE*JG+@~33-dhP+p>B zp8SIJkf=H3A_y=r&vOp|w53Cg`Dh-kDJw`OBS&W8wW}j8wPvcNotDUvT^+L2J^!8U zbw?AWmTrda0j}h+17nTC5A~H!quG{`Hx?Jt-w}^SGx11!q3ef1H_JX`N*T8RfPvkt z)bCcJg~-1JtH&Caje{*MS7+kV+jcs-VJbkHI|o4{yZ8k_Bi)Xp-%w!mJ zzs<sn2xn1u+2d|Z0ZFMNc^8B?+^c|}!XQlrL5AGJgz(FVD zC%4-dLTGJAyfj0&azN&c+OGi41rU0K1Ka`t8StFrGyv&S3)~6DBZFNu`XG~hT-pF5 zKV+u^x6N4g!N+I01ppsPur@)h)j%n9Hz=KhKfUdB7WypANY_zovgYg^Z+1?w7S!)!$Ax7CPK&~ zaO(+DwNF?8G}6yy`^yXRx+3#95#nOr&vDuL@dHT1{o$uR{a7^b%a}|i?uDsvF1*yI z7jh|iy=dM`fDa@>O#fEVd{+PuHiC1f_y>SY8VwCMiRNnyd`8bZ0Qoe1N;J=cz(-mz zKf5>Dpuu_9rt-fd+ON + + + + + + + + + + + + + + + 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..7b4ae58d --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bottom_sheet.xml b/app/src/main/res/layout/bottom_sheet.xml new file mode 100644 index 00000000..a8ab941d --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ 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..66edb168 --- /dev/null +++ b/app/src/main/res/layout/place_item.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/saved_place_item.xml b/app/src/main/res/layout/saved_place_item.xml new file mode 100644 index 00000000..fbba2754 --- /dev/null +++ b/app/src/main/res/layout/saved_place_item.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 768b058a..53d8fe9d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,6 @@ #FF000000 + #7C7C7C #FFFFFFFF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..290224da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ Map + 검색어를 입력해 주세요. + 검색 결과가 없습니다. \ No newline at end of file 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 @@