From 78f85329d0bddfe9501e8b1872df952aee51e446 Mon Sep 17 00:00:00 2001 From: fiveJinw Date: Wed, 31 Jul 2024 20:47:40 +0900 Subject: [PATCH 01/10] init : Copy week 5 code --- .gitignore | 7 +- app/build.gradle.kts | 32 +- .../tech/kakao/map/view/MapActivityTest.kt | 69 +++++ .../tech/kakao/map/view/SearchActivityTest.kt | 196 ++++++++++++ app/src/main/AndroidManifest.xml | 12 +- .../campus/tech/kakao/map/MainApplication.kt | 13 + .../tech/kakao/map/api/KakaoApiDataSource.kt | 53 ++++ .../tech/kakao/map/api/KakaoLocalApi.kt | 13 + .../kakao/map/data/CoroutineDispatcher.kt | 6 + .../java/campus/tech/kakao/map/data/Place.kt | 17 ++ .../tech/kakao/map/data/PlaceRepository.kt | 24 ++ .../tech/kakao/map/data/PositionDataSource.kt | 34 +++ .../campus/tech/kakao/map/data/SavedPlace.kt | 10 + .../tech/kakao/map/data/SavedPlaceDao.kt | 21 ++ .../tech/kakao/map/data/SavedPlaceDatabase.kt | 12 + .../kakao/map/data/SavedPlaceRepository.kt | 27 ++ .../tech/kakao/map/di/DispatcherModule.kt | 18 ++ .../kakao/map/di/LocalDataSourceModule.kt | 45 +++ .../campus/tech/kakao/map/di/NetworkModule.kt | 18 ++ .../tech/kakao/map/utilities/Constants.kt | 16 + .../tech/kakao/map/utilities/PlaceContract.kt | 13 + .../tech/kakao/map/view/ClickListener.kt | 13 + .../campus/tech/kakao/map/view/MapActivity.kt | 214 +++++++++++++ .../tech/kakao/map/view/PlaceViewAdapter.kt | 54 ++++ .../kakao/map/view/SavedPlaceViewAdapter.kt | 62 ++++ .../tech/kakao/map/view/SearchActivity.kt | 154 ++++++++++ .../map/viewmodel/MapActivityViewModel.kt | 45 +++ .../map/viewmodel/SearchActivityViewModel.kt | 79 +++++ .../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 | 75 +++++ app/src/main/res/layout/activity_search.xml | 96 ++++++ app/src/main/res/layout/bottom_sheet.xml | 46 +++ app/src/main/res/layout/place_item.xml | 71 +++++ app/src/main/res/layout/saved_place_item.xml | 48 +++ 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 | 40 +++ .../repository/SavedPlaceRepositoryTest.kt | 76 +++++ .../viewmodel/SearchActivityViewModelTest.kt | 81 +++++ 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 +- 47 files changed, 1982 insertions(+), 145 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/api/KakaoApiDataSource.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/api/KakaoLocalApi.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/CoroutineDispatcher.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/Place.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/PositionDataSource.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/SavedPlace.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDao.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDatabase.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/SavedPlaceRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/di/DispatcherModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/di/LocalDataSourceModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/utilities/Constants.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/utilities/PlaceContract.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/PlaceViewAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/SavedPlaceViewAdapter.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/SearchActivityViewModel.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 803085bd..8af80336 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,17 +1,20 @@ +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") - id("com.google.gms.google-services") + id("kotlin-parcelize") } android { namespace = "campus.tech.kakao.map" compileSdk = 34 + defaultConfig { applicationId = "campus.tech.kakao.map" minSdk = 26 @@ -19,7 +22,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 { @@ -41,12 +49,13 @@ 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") @@ -55,26 +64,27 @@ dependencies { 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.activity:activity-ktx:1.9.1") 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") - implementation(platform("com.google.firebase:firebase-bom:33.1.2")) - implementation("com.google.firebase:firebase-analytics-ktx") - implementation("com.google.firebase:firebase-config-ktx:22.0.0") - implementation("com.google.firebase:firebase-messaging-ktx:24.0.0") testImplementation("androidx.room:room-testing:2.6.1") + implementation("androidx.datastore:datastore-preferences:1.1.1") 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") + 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") 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") @@ -82,4 +92,4 @@ 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") -} +} \ 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..a21a03e8 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/view/SearchActivityTest.kt @@ -0,0 +1,196 @@ +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.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.utilities.Constants +import campus.tech.kakao.map.data.Place +import campus.tech.kakao.map.data.SavedPlace +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + + +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 f2e7b45a..d2438a55 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,6 @@ - - + @@ -25,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..efc54cb1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MainApplication.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map + +import android.app.Application +import com.kakao.vectormap.KakaoMapSdk +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +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/api/KakaoApiDataSource.kt b/app/src/main/java/campus/tech/kakao/map/api/KakaoApiDataSource.kt new file mode 100644 index 00000000..5abae54c --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/api/KakaoApiDataSource.kt @@ -0,0 +1,53 @@ +package campus.tech.kakao.map.api + +import android.util.Log +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.data.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/api/KakaoLocalApi.kt b/app/src/main/java/campus/tech/kakao/map/api/KakaoLocalApi.kt new file mode 100644 index 00000000..f0000403 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/api/KakaoLocalApi.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.api + +import campus.tech.kakao.map.data.ResultSearch +import retrofit2.http.GET +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/data/CoroutineDispatcher.kt b/app/src/main/java/campus/tech/kakao/map/data/CoroutineDispatcher.kt new file mode 100644 index 00000000..c3b7148b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/CoroutineDispatcher.kt @@ -0,0 +1,6 @@ +package campus.tech.kakao.map.data + +import javax.inject.Qualifier + +@Qualifier +annotation class CoroutineIoDispatcher \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/Place.kt b/app/src/main/java/campus/tech/kakao/map/data/Place.kt new file mode 100644 index 00000000..458ea00e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/Place.kt @@ -0,0 +1,17 @@ +package campus.tech.kakao.map.data + +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/data/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt new file mode 100644 index 00000000..b43e8f05 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepository.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.data + +import campus.tech.kakao.map.api.KakaoApiDataSource +import campus.tech.kakao.map.data.Place +import com.kakao.vectormap.LatLng +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +class PlaceRepository @Inject constructor(private val kakaoApiDataSource : KakaoApiDataSource, private val positionDataSource : PositionDataSource){ + suspend fun getKakaoLocalPlaceData(text : String) : List{ + val placeList = kakaoApiDataSource.getPlaceData(text) + return placeList + } + + suspend fun saveCurrentPosToDataStore(latitude : Double, longitude : Double){ + positionDataSource.putPos(latitude, longitude) + } + + suspend fun fetchLastPosFromDataStore() : LatLng{ + val lastPos = positionDataSource.pos.first() + return lastPos + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/PositionDataSource.kt b/app/src/main/java/campus/tech/kakao/map/data/PositionDataSource.kt new file mode 100644 index 00000000..faa50818 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/PositionDataSource.kt @@ -0,0 +1,34 @@ +package campus.tech.kakao.map.data + +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.utilities.Constants +import campus.tech.kakao.map.view.MapActivity +import com.kakao.vectormap.LatLng +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PositionDataSource @Inject constructor(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] ?: Constants.ChonnamUnivLocation.LATITUDE.toDouble() + val longitude = preferences[KEY_LONGITUDE] ?: Constants.ChonnamUnivLocation.LONGITUDE.toDouble() + LatLng.from(latitude, longitude) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedPlace.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedPlace.kt new file mode 100644 index 00000000..2adddc44 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedPlace.kt @@ -0,0 +1,10 @@ +package campus.tech.kakao.map.data + +import androidx.room.Entity +import androidx.room.PrimaryKey +import campus.tech.kakao.map.utilities.PlaceContract + +@Entity(tableName = PlaceContract.SavedPlaceEntry.TABLE_NAME) +data class SavedPlace( + @PrimaryKey val name: String +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDao.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDao.kt new file mode 100644 index 00000000..709c1d5e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDao.kt @@ -0,0 +1,21 @@ +package campus.tech.kakao.map.data + +import androidx.room.Dao +import androidx.room.Query +import campus.tech.kakao.map.utilities.PlaceContract + +@Dao +interface SavedPlaceDao { + + @Query("SELECT * FROM ${PlaceContract.SavedPlaceEntry.TABLE_NAME}") + fun readSavedPlaceData(): List + + @Query("SELECT * FROM ${PlaceContract.SavedPlaceEntry.TABLE_NAME} WHERE ${PlaceContract.SavedPlaceEntry.COLUMN_NAME} = :name") + fun readSavedPlaceDataWithSamedName(name: String): List + + @Query("DELETE FROM ${PlaceContract.SavedPlaceEntry.TABLE_NAME} WHERE ${PlaceContract.SavedPlaceEntry.COLUMN_NAME} = :name") + fun deleteSavedPlace(name: String) + + @Query("INSERT INTO ${PlaceContract.SavedPlaceEntry.TABLE_NAME} (${PlaceContract.SavedPlaceEntry.COLUMN_NAME}) VALUES (:name)") + fun insertSavedPlaceData(name:String) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDatabase.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDatabase.kt new file mode 100644 index 00000000..154718a6 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceDatabase.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map.data + +import androidx.room.Database +import androidx.room.RoomDatabase +import campus.tech.kakao.map.utilities.PlaceContract + +@Database(entities = [SavedPlace::class], version = PlaceContract.VERSION, exportSchema = false) +abstract class SavedPlaceDatabase : RoomDatabase() { + + abstract fun savedPlaceDao(): SavedPlaceDao +} + diff --git a/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceRepository.kt new file mode 100644 index 00000000..181596c0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/SavedPlaceRepository.kt @@ -0,0 +1,27 @@ +package campus.tech.kakao.map.data + +import android.util.Log +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SavedPlaceRepository @Inject constructor(private val savedPlaceDao : SavedPlaceDao){ + suspend fun getAllSavedPlace() : List = savedPlaceDao.readSavedPlaceData() + + + suspend fun writePlace(place: Place){ + val savedList = savedPlaceDao.readSavedPlaceDataWithSamedName(place.name) + if (savedList.isNotEmpty()) { + Log.d("testt", "데이터 중복") + // 입력의 시간순대로 정렬되기 떄문에 레코드 삭제후 다시 집어넣기 + savedPlaceDao.deleteSavedPlace(place.name) + savedPlaceDao.insertSavedPlaceData(place.name) + } else { + savedPlaceDao.insertSavedPlaceData(place.name) + } + } + + suspend fun deleteSavedPlace(savedPlace: SavedPlace){ + savedPlaceDao.deleteSavedPlace(savedPlace.name) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/di/DispatcherModule.kt b/app/src/main/java/campus/tech/kakao/map/di/DispatcherModule.kt new file mode 100644 index 00000000..272fb9e0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/di/DispatcherModule.kt @@ -0,0 +1,18 @@ +package campus.tech.kakao.map.di + +import campus.tech.kakao.map.data.CoroutineIoDispatcher +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +@Module +@InstallIn(SingletonComponent::class) +internal object DispatchersModule { + + @Provides + @CoroutineIoDispatcher + fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/di/LocalDataSourceModule.kt b/app/src/main/java/campus/tech/kakao/map/di/LocalDataSourceModule.kt new file mode 100644 index 00000000..30e1fb75 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/di/LocalDataSourceModule.kt @@ -0,0 +1,45 @@ +package campus.tech.kakao.map.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import androidx.room.Room +import campus.tech.kakao.map.utilities.Constants +import campus.tech.kakao.map.data.SavedPlaceDao +import campus.tech.kakao.map.data.SavedPlaceDatabase +import campus.tech.kakao.map.utilities.PlaceContract +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +class LocalDataSourceModule { + + @Singleton + @Provides + fun provideRoomDatabase(@ApplicationContext context: Context): SavedPlaceDatabase { + return Room.databaseBuilder( + context, SavedPlaceDatabase::class.java, PlaceContract.DATABASE_NAME + ).build() + } + + @Provides + fun providePlantDao(savedPlaceDatabase: SavedPlaceDatabase): SavedPlaceDao { + return savedPlaceDatabase.savedPlaceDao() + } + + private val Context.dataStore: DataStore by preferencesDataStore(name = Constants.DataStore.PREFERENCES_NAME) + + @Singleton + @Provides + fun provideDataStore(@ApplicationContext context: Context) : DataStore{ + return context.dataStore + } + + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt b/app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt new file mode 100644 index 00000000..dd399c41 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt @@ -0,0 +1,18 @@ +package campus.tech.kakao.map.di + +import campus.tech.kakao.map.api.KakaoApiDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class NetworkModule{ + @Singleton + @Provides + fun provideKakaoApiDataSource(): KakaoApiDataSource { + return KakaoApiDataSource() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/utilities/Constants.kt b/app/src/main/java/campus/tech/kakao/map/utilities/Constants.kt new file mode 100644 index 00000000..4f40a91e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/utilities/Constants.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map.utilities + +class Constants { + object Keys{ + const val KEY_PLACE = "place" + } + + object DataStore{ + const val PREFERENCES_NAME = "location_preferences" + } + + object ChonnamUnivLocation { + const val LATITUDE = "35.175487" + const val LONGITUDE = "126.907163" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/utilities/PlaceContract.kt b/app/src/main/java/campus/tech/kakao/map/utilities/PlaceContract.kt new file mode 100644 index 00000000..742f885a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/utilities/PlaceContract.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.utilities + +import android.provider.BaseColumns + +object PlaceContract{ + const val DATABASE_NAME = "PLACE.DB" + const val VERSION = 1 + + object SavedPlaceEntry : BaseColumns{ + const val TABLE_NAME = "SAVED_PLACE" + const val COLUMN_NAME = "name" + } +} \ 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..29feefae --- /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.data.Place +import campus.tech.kakao.map.data.SavedPlace + +interface OnClickPlaceListener { + fun savePlace(place: Place) +} + +interface OnClickSavedPlaceListener { + fun deleteSavedPlace(savedPlace: SavedPlace) + 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..ec2e4112 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/MapActivity.kt @@ -0,0 +1,214 @@ +package campus.tech.kakao.map.view + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.isVisible +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivityMapBinding +import campus.tech.kakao.map.databinding.BottomSheetBinding +import campus.tech.kakao.map.utilities.Constants +import campus.tech.kakao.map.data.Place +import campus.tech.kakao.map.viewmodel.MapActivityViewModel +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 +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class MapActivity : AppCompatActivity() { + + private lateinit var binding : ActivityMapBinding + private lateinit var bottomSheetBinding : BottomSheetBinding + lateinit var map: MapView + lateinit var kakaoMap : KakaoMap + lateinit var resultLauncher : ActivityResultLauncher + lateinit var bottomSheetBehavior : BottomSheetBehavior + private val viewModel : MapActivityViewModel by viewModels() + var isMapDisplay = false + var isInitState = true + + 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) + + initVar() + initMapView() + initBottomSheet() + initClickListener() + initResultLauncher() + initObserver() + + } + + + private fun initVar() { + binding = DataBindingUtil.setContentView(this, R.layout.activity_map) + binding.viewModel = viewModel + binding.lifecycleOwner = this + bringFrontSearchField() + } + + + private fun initBottomSheet(){ + bottomSheetBinding = binding.bottomSheetInclude + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetBinding.bottomSheet) + 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") + binding.errorText.isVisible = false + this@MapActivity.kakaoMap = kakaoMap + isMapDisplay = true + viewModel.getRecentPos() + } + }) + } + + private fun bringFrontSearchField() { + binding.inputSearchField.bringToFront() + binding.searchIcon.bringToFront() + } + + private fun initClickListener() { + binding.inputSearchField.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() ?: Constants.ChonnamUnivLocation.LATITUDE.toDouble() + val longitude = place?.x?.toDouble()?: Constants.ChonnamUnivLocation.LONGITUDE.toDouble() + val pos = LatLng.from(latitude, longitude) + Log.d("testtt", "LAT" + pos.toString()) + isInitState = false + bottomSheetBinding.place = place + Log.d("placeTest", "Place : ${place}, Binding : ${bottomSheetBinding.place}") + viewModel.setRecentPos(latitude, longitude) + } + } + } + + fun initObserver(){ + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED){ + viewModel.recentPos.collect{ + Log.d("testtt", "밖" + it.toString()) + if(isMapDisplay and !isInitState) { + Log.d("testtt", "안" + it.toString()) + moveMapCamera(it) + createLabel(it) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + else 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) { + binding.errorText.isVisible = true + val errorCode = getErrorCode(error) + val errorText = ErrorCode.getErrorMessage(errorCode).errorMessage + "\n\n" + error + binding.errorText.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 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/PlaceViewAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/PlaceViewAdapter.kt new file mode 100644 index 00000000..8f9e0b95 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/PlaceViewAdapter.kt @@ -0,0 +1,54 @@ +package campus.tech.kakao.map.view + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.PlaceItemBinding +import campus.tech.kakao.map.data.Place + +class PlaceViewAdapter( + val listener: OnClickPlaceListener +) : ListAdapter(PlaceDiffCallBack()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = DataBindingUtil.inflate(inflater, R.layout.place_item, parent, false) + return PlaceViewHolder(binding, listener) + } + + override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) { + 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(private val binding : PlaceItemBinding, listener : OnClickPlaceListener) : + RecyclerView.ViewHolder(binding.root) { + init { + binding.listener = listener + } + + fun bind(place : Place){ + binding.place = place + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/view/SavedPlaceViewAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/SavedPlaceViewAdapter.kt new file mode 100644 index 00000000..783f8ec4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SavedPlaceViewAdapter.kt @@ -0,0 +1,62 @@ +package campus.tech.kakao.map.view + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.SavedPlaceItemBinding +import campus.tech.kakao.map.data.SavedPlace + +class SavedPlaceViewAdapter( + val listener: OnClickSavedPlaceListener +) : ListAdapter(SavedPlaceDiffCallBack()) { + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedPlaceViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = DataBindingUtil.inflate(inflater, R.layout.saved_place_item, parent, false) + Log.d("testt", "저장된 장소를 띄우는 뷰 홀더 생성") + return SavedPlaceViewHolder(binding, 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(private val binding: SavedPlaceItemBinding, listener: OnClickSavedPlaceListener) : + RecyclerView.ViewHolder(binding.root) { + + init { + binding.listener = listener + } + + fun bind(savedPlace: SavedPlace) { + binding.savedPlace = savedPlace + } + +} \ 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..3a291919 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SearchActivity.kt @@ -0,0 +1,154 @@ +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.ImageView +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivitySearchBinding +import campus.tech.kakao.map.utilities.Constants +import campus.tech.kakao.map.data.Place +import campus.tech.kakao.map.data.SavedPlace +import campus.tech.kakao.map.viewmodel.SearchActivityViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SearchActivity : AppCompatActivity(), OnClickPlaceListener, OnClickSavedPlaceListener { + private lateinit var binding : ActivitySearchBinding + lateinit var savedPlaceRecyclerViewAdapter: SavedPlaceViewAdapter + lateinit var searchRecyclerViewAdapter: PlaceViewAdapter + private val viewModel: SearchActivityViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + initBinding() + initListeners() + initRecyclerViews() + initObserver() + binding.inputSearchField.requestFocus() + } + + override fun deleteSavedPlace(savedPlace: SavedPlace) { + Log.d("testt", "삭제 콜백함수 처리") + viewModel.deleteSavedPlace(savedPlace) + } + + override fun loadPlace(savedPlace: SavedPlace){ + binding.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 initBinding() { + binding = DataBindingUtil.setContentView(this, R.layout.activity_search) + binding.viewModel = viewModel + binding.lifecycleOwner = this + } + + fun initListeners() { + initDeleteButtonListener() + initInputFieldListener() + } + + fun initDeleteButtonListener() { + binding.buttonX.setOnClickListener { + binding.inputSearchField.setText("") + binding.inputSearchField.clearFocus() + binding.inputSearchField.parent.clearChildFocus(binding.inputSearchField) + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(window.decorView.applicationWindowToken, 0) + } + } + + fun initInputFieldListener() { + binding.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) + binding.searchResultRecyclerView.layoutManager = LinearLayoutManager(this) + binding.searchResultRecyclerView.adapter = searchRecyclerViewAdapter + } + + fun initSavedPlaceRecyclerView() { + savedPlaceRecyclerViewAdapter = + SavedPlaceViewAdapter(this) + binding.savedSearchRecyclerView.layoutManager = + LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) + binding.savedSearchRecyclerView.adapter = savedPlaceRecyclerViewAdapter + } + + fun initObserver(){ + initPlaceObserver() + initSavedPlaceObserver() + } + + fun initPlaceObserver(){ + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.place.collect { placeList -> + searchRecyclerViewAdapter.submitList(placeList) + } + } + } + } + + fun initSavedPlaceObserver(){ + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.savedPlace.collect { savedPlaceList -> + savedPlaceRecyclerViewAdapter.submitList(savedPlaceList) + if (savedPlaceList.isEmpty()) binding.savedSearchRecyclerView.visibility = View.GONE + else binding.savedSearchRecyclerView.visibility = View.VISIBLE + } + } + } + } +} + + + + + 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..9a2c3975 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapActivityViewModel.kt @@ -0,0 +1,45 @@ +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.data.PlaceRepository +import campus.tech.kakao.map.data.PositionDataSource +import campus.tech.kakao.map.utilities.Constants +import com.kakao.vectormap.LatLng +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MapActivityViewModel @Inject constructor( + private val placeRepository: PlaceRepository +) : ViewModel() { + private val _recentPos = MutableStateFlow( + LatLng.from( + Constants.ChonnamUnivLocation.LATITUDE.toDouble(), + Constants.ChonnamUnivLocation.LONGITUDE.toDouble() + ) + ) + val recentPos: StateFlow get() = _recentPos + + fun getRecentPos(){ + viewModelScope.launch{ + _recentPos.value = placeRepository.fetchLastPosFromDataStore() + Log.d("testtt", "recent" + recentPos.value.toString()) + + } + } + + fun setRecentPos(latitude : Double, longitude : Double){ + viewModelScope.launch { + placeRepository.saveCurrentPosToDataStore(latitude, longitude) + getRecentPos() + } + } +} \ 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..d5ed4133 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SearchActivityViewModel.kt @@ -0,0 +1,79 @@ +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.data.CoroutineIoDispatcher +import campus.tech.kakao.map.data.Place +import campus.tech.kakao.map.data.SavedPlace +import campus.tech.kakao.map.data.PlaceRepository +import campus.tech.kakao.map.data.SavedPlaceRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class UiState( + val isPlaceListEmpty : Boolean = true +) + + +@HiltViewModel +class SearchActivityViewModel @Inject constructor( + private val placeRepository: PlaceRepository, + private val savedPlaceRepository: SavedPlaceRepository, + @CoroutineIoDispatcher private val IoDispatcher : CoroutineDispatcher +) : ViewModel() { + private val _place = MutableStateFlow>(emptyList()) + private val _savedPlace = MutableStateFlow>(emptyList()) + val place: StateFlow> get() = _place.asStateFlow() + val savedPlace: StateFlow> get() = _savedPlace.asStateFlow() + private val _uiState = MutableStateFlow(UiState()) + val uiState : StateFlowget() = _uiState.asStateFlow() + + init { + getSavedPlace() + + } + + fun getSavedPlace() { + viewModelScope.launch(IoDispatcher) { + _savedPlace.value = savedPlaceRepository.getAllSavedPlace() + } + } + + fun savePlace(place: Place) { + viewModelScope.launch(IoDispatcher) { + savedPlaceRepository.writePlace(place) + getSavedPlace() + } + } + + fun deleteSavedPlace(savedPlace: SavedPlace) { + viewModelScope.launch(IoDispatcher){ + savedPlaceRepository.deleteSavedPlace(savedPlace) + getSavedPlace() + } + } + + suspend fun getKakaoLocalData(text: String) { + if (text.isNotEmpty()) { + val placeList = placeRepository.getKakaoLocalPlaceData(text) + _place.value = (placeList) + } else _place.value = listOf() + + _uiState.update {currentState -> + currentState.copy( + isPlaceListEmpty = if(_place.value.isEmpty()) true else false + ) + } + Log.d("testt", "value : ${uiState.value}") + } +} \ 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 + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..253b0b79 --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..48a2ecdf --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + \ 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..fb8ef55a --- /dev/null +++ b/app/src/main/res/layout/place_item.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + \ 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..e7357cfe --- /dev/null +++ b/app/src/main/res/layout/saved_place_item.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + \ 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 @@