From 4add9dea2705d6fed881c9b98bfd1ec3bc0191bd Mon Sep 17 00:00:00 2001 From: kimseongyu <50648835+kimseongyu@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:10:27 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B6=A9=EB=82=A8=EB=8C=80=20Android=5F?= =?UTF-8?q?=EA=B9=80=EC=84=A0=EA=B7=9C=206=EC=A3=BC=EC=B0=A8=20Step0=20(#2?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit * 충남대 Android_김선규 5주차 Step0 (#33) * Initial commit * 충남대 Android_김선규 4주차 Step0 (#10) * Initial commit * Merge : android-map-keyword into android-map-search (#8) * 충남대 Android_김선규 3주차 과제 Step1 (#47) * docs: add step1 requirements * chore: set for using android api * style: rename id in layout * feat: remove storeInfo for using api * feat: add connecting api for searching * style: rename variable name proper * 충남대 Android_김선규 3주차 과제 Step2 (#85) * style: function rename and split * feat: Change function to fit coroutine * docs: add step2 requirements * style: move from main to sub file * chore: set it up to work in the right environment * feat: display kakao map, when app is started --------- Co-authored-by: MyStoryG * 충남대 Android_김선규 4주차 Step 1 제출 (#47) * docs: add week 4 step 1 requirements * feat: add searching by saved search keyword * chore: relocate files proper * feat: modify adapter to make clean code * feat: add image for marker * feat: add layout for displaying bottom sheet * feat: add parcelabel for easy to send data * feat: add displaying search result * refactor: modify class structure * feat: add error screen and reload button * feat: add saving and loading last position when app is closed, save last position when app is opened, load last position * 충남대 Android_김선규 4주차 Step2 수정 (#73) * style: rename variable name * feat: add viewModel and Repository for saving last position * refactor: classify in more detail * test: add android UI test * chore: add mockk test dependency * test: add ViewModel test * chore: add testOptions * test: modify android ui test --------- Co-authored-by: MyStoryG * 충남대 Android_김선규 4주차 Step0 (#10) * Initial commit * Merge : android-map-keyword into android-map-search (#8) * 충남대 Android_김선규 3주차 과제 Step1 (#47) * docs: add step1 requirements * chore: set for using android api * style: rename id in layout * feat: remove storeInfo for using api * feat: add connecting api for searching * style: rename variable name proper * 충남대 Android_김선규 3주차 과제 Step2 (#85) * style: function rename and split * feat: Change function to fit coroutine * docs: add step2 requirements * style: move from main to sub file * chore: set it up to work in the right environment * feat: display kakao map, when app is started --------- Co-authored-by: MyStoryG * 충남대 Android_김선규 4주차 Step 1 제출 (#47) * docs: add week 4 step 1 requirements * feat: add searching by saved search keyword * chore: relocate files proper * feat: modify adapter to make clean code * feat: add image for marker * feat: add layout for displaying bottom sheet * feat: add parcelabel for easy to send data * feat: add displaying search result * refactor: modify class structure * feat: add error screen and reload button * feat: add saving and loading last position when app is closed, save last position when app is opened, load last position * 충남대 Android_김선규 4주차 Step2 수정 (#73) * style: rename variable name * feat: add viewModel and Repository for saving last position * refactor: classify in more detail * test: add android UI test * chore: add mockk test dependency * test: add ViewModel test * chore: add testOptions * test: modify android ui test * chore: add Room dependency * feat: change SQLite to Room about DB * chore: add Hilt dependency * feat: add Hilt for MVVM * chore: add dataBinding * feat: add dataBinding * feat: modify about Hilt --------- Co-authored-by: MyStoryG --- README.md | 75 ++++++++ app/build.gradle.kts | 21 ++- .../tech/kakao/map/KakaoMapActivityTest.kt | 88 ++++++++++ .../campus/tech/kakao/map/SearchWindowTest.kt | 96 ++++++++++ app/src/main/AndroidManifest.xml | 11 +- .../java/campus/tech/kakao/map/AppModule.kt | 35 ++++ .../campus/tech/kakao/map/MainActivity.kt | 2 + .../campus/tech/kakao/map/MyApplication.kt | 8 + .../kakao/map/model/search/SearchKeyword.kt | 38 ++++ .../kakao/map/model/search/SearchResults.kt | 22 +++ .../kakaomap/LastPositionRepository.kt | 33 ++++ .../kakaomap/SharedPreferencesModule.kt | 23 +++ .../kakao/map/repository/search/DBModule.kt | 27 +++ .../map/repository/search/KakaoAPISetting.kt | 20 +++ .../map/repository/search/RetrofitModule.kt | 24 +++ .../search/SavedSearchKeywordRepository.kt | 30 ++++ .../map/repository/search/SearchKeywordDB.kt | 14 ++ .../map/repository/search/SearchRepository.kt | 22 +++ .../tech/kakao/map/view/ActivityKeys.kt | 7 + .../tech/kakao/map/view/MainActivity.kt | 17 ++ .../map/view/kakaomap/KakaoMapActivity.kt | 166 ++++++++++++++++++ .../view/search/SavedSearchKeywordsAdapter.kt | 59 +++++++ .../map/view/search/SearchResultsAdapter.kt | 54 ++++++ .../map/view/search/SearchWindowActivity.kt | 127 ++++++++++++++ .../viewmodel/kakaomap/KakaoMapViewModel.kt | 29 +++ .../map/viewmodel/search/SearchViewModel.kt | 55 ++++++ .../drawable/ic_custom_magnifying_glass.xml | 12 ++ .../main/res/drawable/ic_custom_reload.xml | 9 + app/src/main/res/drawable/marker.png | Bin 0 -> 4115 bytes .../main/res/layout/activity_kakao_map.xml | 107 +++++++++++ app/src/main/res/layout/activity_main.xml | 12 +- .../activity_place_info_bottom_sheet.xml | 41 +++++ .../res/layout/activity_search_window.xml | 75 ++++++++ .../res/layout/saved_search_keyword_item.xml | 33 ++++ .../main/res/layout/search_result_item.xml | 70 ++++++++ app/src/main/res/values/strings.xml | 3 + .../tech/kakao/map/KakaoMapViewModelTest.kt | 52 ++++++ .../tech/kakao/map/SearchViewModelTest.kt | 112 ++++++++++++ build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 3 +- 40 files changed, 1618 insertions(+), 16 deletions(-) create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/KakaoMapActivityTest.kt create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/SearchWindowTest.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/AppModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/MyApplication.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/search/SearchKeyword.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/search/SearchResults.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/kakaomap/LastPositionRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/kakaomap/SharedPreferencesModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/search/DBModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/search/KakaoAPISetting.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/search/RetrofitModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/search/SavedSearchKeywordRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/search/SearchKeywordDB.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/search/SearchRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/ActivityKeys.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/MainActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/kakaomap/KakaoMapActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/search/SavedSearchKeywordsAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/search/SearchResultsAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/search/SearchWindowActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/kakaomap/KakaoMapViewModel.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/search/SearchViewModel.kt create mode 100644 app/src/main/res/drawable/ic_custom_magnifying_glass.xml create mode 100644 app/src/main/res/drawable/ic_custom_reload.xml create mode 100644 app/src/main/res/drawable/marker.png create mode 100644 app/src/main/res/layout/activity_kakao_map.xml create mode 100644 app/src/main/res/layout/activity_place_info_bottom_sheet.xml create mode 100644 app/src/main/res/layout/activity_search_window.xml create mode 100644 app/src/main/res/layout/saved_search_keyword_item.xml create mode 100644 app/src/main/res/layout/search_result_item.xml create mode 100644 app/src/test/java/campus/tech/kakao/map/KakaoMapViewModelTest.kt create mode 100644 app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt diff --git a/README.md b/README.md index 99676874..41c4acf2 100644 --- a/README.md +++ b/README.md @@ -1 +1,76 @@ # android-map-notification + +## Layout requirements + +**Kakao map** + +Display kakao Map + +- Display selected search result using `bottom sheet` +- When onMapError() is called, print error message +- Save last location before app closed, and when app is started, focusing that location + +Search window button + +- If Search window button is clicked, go to _Save search keyword_ + - _Stack Save search keyword_ on top of the this window + +**Save search keyword** + +Input search keyword + +- Text input window to search +- It has x button to erase + +Saved search keyword list + +- It has x button to erase +- It scrolls horizontally + +Search result list + +- Using `RecyclerView` to implement about search result list +- It scrolls vertically + +## Function List + +**Kakao map** + +**Kakao map** + +Display kakao Map + +- Display selected search result using `bottom sheet` +- When onMapError() is called, print error message +- Save last location before app closed, and when app is started, focusing that location + +Go to _Save serach keyword_ + +- If Search window button is clicked, go to _Save search keyword_ + - _Stack Save search keyword_ on top of the this window + +**Save search keyword** + +Requirements Rule + +- Using `SQLite` to save search data + - When application is restart, data is maintained +- Apply the MVVM architectural pattern + +Input search keyword + +- When application is restart, data is maintained +- Search every time a character is entered +- When clicked x button, string is erased + +Saved search keyword list + +- Keywords are not duplicated and recently serached keyword are added later +- Can search to select saved search keyword +- When clicked x button, saved search keyword is erased + +Search result list + +- There are at least 15 search results +- Search results have search word as categories +- Selected item is added to the saved search word list, and display location on the kakao map diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 803085bd..7ae06650 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,7 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + +fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key) + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -5,7 +9,7 @@ plugins { id("kotlin-parcelize") id("kotlin-kapt") id("com.google.dagger.hilt.android") - id("com.google.gms.google-services") +// id("com.google.gms.google-services") } android { @@ -20,6 +24,16 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY")) + buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY")) + + ndk { + abiFilters.add("arm64-v8a") + abiFilters.add("armeabi-v7a") + abiFilters.add("x86") + abiFilters.add("x86_64") + } } buildTypes { @@ -43,6 +57,11 @@ android { dataBinding = true buildConfig = true } + testOptions { + packaging { + resources.excludes.add("META-INF/*") + } + } } dependencies { diff --git a/app/src/androidTest/java/campus/tech/kakao/map/KakaoMapActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/KakaoMapActivityTest.kt new file mode 100644 index 00000000..64381f7c --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/KakaoMapActivityTest.kt @@ -0,0 +1,88 @@ +package campus.tech.kakao.map + +import android.app.Instrumentation +import android.content.Intent +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import campus.tech.kakao.map.model.search.Place +import campus.tech.kakao.map.view.ActivityKeys +import campus.tech.kakao.map.view.kakaomap.KakaoMapActivity +import kotlinx.coroutines.delay +import org.hamcrest.Matchers.not +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +private const val TEST_PLACE_NAME = "Test Place" +private const val TEST_CATEGORY_NAME = "Test Category" +private const val TEST_ADDRESS_NAME = "Test Address" +private const val TEST_X = "36.37003" +private const val TEST_Y = "127.34594" + +@RunWith(AndroidJUnit4::class) +class KakaoMapActivityTest { + + @get: Rule + val activityRule = ActivityScenarioRule(KakaoMapActivity::class.java) + + private lateinit var mDvice: UiDevice + + @Before + fun setUp() { + ActivityScenario.launch(KakaoMapActivity::class.java) + mDvice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + } + + @Test + fun testKakaoMapIsLoaded() { + // then + onView(withId(R.id.kakaomap_err)).check(matches(not(isDisplayed()))) + } + + @Test + fun testDisplaySearchWindow() { + // when + onView(withId(R.id.goto_search_window)).perform(click()) + + // then + onView(withId(R.id.searchWindow)).check(matches(isDisplayed())) + } + + @Test + fun testDisplayPlaceOnMap() { + // given + val place = Place(TEST_PLACE_NAME, TEST_CATEGORY_NAME, TEST_ADDRESS_NAME, TEST_X, TEST_Y) + val intent = Intent( + InstrumentationRegistry.getInstrumentation().targetContext, + KakaoMapActivity::class.java + ).apply { + putExtra(ActivityKeys.INTENT_PLACE, place) + } + + // when + ActivityScenario.launch(intent).use { + mDvice.waitForIdle(3000) + + // then + onView(withId(R.id.place_info_bottom_sheet)).check(matches(isDisplayed())) + } + } + + @Test + fun testDisplayKakaoMapError() { + // 잘못된 API 전송하도록 설정 + // onView(withId(R.id.kakaomap_err)).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/SearchWindowTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/SearchWindowTest.kt new file mode 100644 index 00000000..c7106689 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/SearchWindowTest.kt @@ -0,0 +1,96 @@ +package campus.tech.kakao.map + +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import campus.tech.kakao.map.model.search.Place +import campus.tech.kakao.map.view.ActivityKeys +import campus.tech.kakao.map.view.search.SearchWindowActivity +import org.hamcrest.Matchers.not +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +private const val TEST_SEARCH_KEYWORD = "cafe" +private const val TEST_PLACE_NAME = "충남대학교 대덕캠퍼스" + +@RunWith(AndroidJUnit4::class) +class SearchWindowTest { + + @get: Rule + val activityRule = ActivityScenarioRule(SearchWindowActivity::class.java) + + @Before + fun setUp() { + ActivityScenario.launch(SearchWindowActivity::class.java) + Intents.init() + } + + @After + fun tearDown() { + Intents.release() + } + + @Test + fun testChangeSearchWindowText() { + // when + onView(withId(R.id.searchWindow)).perform( + typeText(TEST_SEARCH_KEYWORD), + closeSoftKeyboard() + ) + + // then + onView(withId(R.id.emptySearchResults)).check(matches(not(isDisplayed()))) + onView(withId(R.id.searchResultsList)).check(matches(isDisplayed())) + } + + @Test + fun testDeleteSearchKeyword() { + // when + onView(withId(R.id.delSearchKeyword)).perform(click()) + + // then + onView(withId(R.id.searchWindow)).check(matches(withText(""))) + } + + @Test + fun testClickSearchResult() { + // when + onView(withId(R.id.searchWindow)).perform( + typeText(TEST_SEARCH_KEYWORD), + closeSoftKeyboard() + ) + Thread.sleep(3000) + onView(withId(R.id.searchResultsList)) + .perform(actionOnItemAtPosition(0, click())) + + // then + Intents.intended(hasExtra(ActivityKeys.INTENT_PLACE, Place::class.java)) + } + + @Test + fun testClickSavedSearchKeyword() { + // recyclerview의 item view 클릭 추가 + // onView(withId(R.id.searchWindow)).check(matches(withText(TEST_PLACE_NAME))) + } + + @Test + fun testDeleteSavedSearchKeyword() { + // recyclerview의 item view 클릭 추가 + // onView(withText(TEST_PLACE_NAME)).check(matches(not(isDisplayed()))) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f2e7b45a..3e8e1904 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + + @@ -27,4 +34,4 @@ - + \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/AppModule.kt b/app/src/main/java/campus/tech/kakao/map/AppModule.kt new file mode 100644 index 00000000..acfb0bdc --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/AppModule.kt @@ -0,0 +1,35 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.content.SharedPreferences +import campus.tech.kakao.map.model.search.SearchKeywordDao +import campus.tech.kakao.map.repository.kakaomap.LastPositionRepository +import campus.tech.kakao.map.repository.search.KakaoSearchKeywordAPI +import campus.tech.kakao.map.repository.search.SavedSearchKeywordRepository +import campus.tech.kakao.map.repository.search.SearchRepository +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 + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideSavedSearchKeywordRepository(searchKeywordDao: SearchKeywordDao) = + SavedSearchKeywordRepository(searchKeywordDao) + + @Provides + @Singleton + fun provideSearchRepository(retrofitKakaoSearchKeyword: KakaoSearchKeywordAPI): SearchRepository = + SearchRepository(retrofitKakaoSearchKeyword) + + @Provides + @Singleton + fun provideLastPositionRepository(sharedPreferences: SharedPreferences) = + LastPositionRepository(sharedPreferences) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt index 95b43803..64a4351a 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -2,7 +2,9 @@ package campus.tech.kakao.map import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/campus/tech/kakao/map/MyApplication.kt b/app/src/main/java/campus/tech/kakao/map/MyApplication.kt new file mode 100644 index 00000000..6772fbd8 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/MyApplication.kt @@ -0,0 +1,8 @@ +package campus.tech.kakao.map + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class MyApplication: Application() { +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/search/SearchKeyword.kt b/app/src/main/java/campus/tech/kakao/map/model/search/SearchKeyword.kt new file mode 100644 index 00000000..f108d120 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/search/SearchKeyword.kt @@ -0,0 +1,38 @@ +package campus.tech.kakao.map.model.search + +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Entity +import androidx.room.Index +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query + +@Entity( + tableName = "search_keyword", + indices = [Index( + value = ["search_keyword"], + unique = true + )] +) +data class SearchKeyword( + @ColumnInfo(name = "search_keyword") + var searchKeyword: String +) { + @PrimaryKey(autoGenerate = true) + var id: Int = 0 +} + +@Dao +interface SearchKeywordDao { + @Query("SELECT * FROM search_keyword") + fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(searchKeyword: SearchKeyword) + + @Delete + fun delete(searchKeyword: SearchKeyword) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/search/SearchResults.kt b/app/src/main/java/campus/tech/kakao/map/model/search/SearchResults.kt new file mode 100644 index 00000000..9e136ced --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/search/SearchResults.kt @@ -0,0 +1,22 @@ +package campus.tech.kakao.map.model.search + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +data class SearchResults( + @SerializedName("documents") + val places: List +) + +@Parcelize +data class Place( + val place_name: String, + val category_name: String, + val address_name: String, + val x: String, + val y: String +): Parcelable { + fun getLng():Double = x.toDoubleOrNull() ?: 0.0 + fun getLat():Double = y.toDoubleOrNull() ?: 0.0 +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/kakaomap/LastPositionRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/kakaomap/LastPositionRepository.kt new file mode 100644 index 00000000..de26d4cb --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/kakaomap/LastPositionRepository.kt @@ -0,0 +1,33 @@ +package campus.tech.kakao.map.repository.kakaomap + +import android.content.Context +import android.content.SharedPreferences +import campus.tech.kakao.map.view.ActivityKeys +import com.google.gson.Gson +import com.google.gson.JsonParseException +import com.kakao.vectormap.LatLng +import javax.inject.Inject + +class LastPositionRepository @Inject constructor(private val sharedPreferences: SharedPreferences) { + + fun saveLastPosition(position: LatLng) { + val editor = sharedPreferences.edit() + val gson = Gson() + val json = gson.toJson(position) + editor.putString(ActivityKeys.INTENT_PLACE, json) + editor.apply() + } + + fun loadLastPosition(): LatLng? { + if (sharedPreferences.contains(ActivityKeys.PREFS_PLACE)) { + val gson = Gson() + val json = sharedPreferences.getString(ActivityKeys.PREFS_PLACE, "") + try { + return gson.fromJson(json, LatLng::class.java) + } catch (e: JsonParseException) { + e.printStackTrace() + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/kakaomap/SharedPreferencesModule.kt b/app/src/main/java/campus/tech/kakao/map/repository/kakaomap/SharedPreferencesModule.kt new file mode 100644 index 00000000..83cd932b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/kakaomap/SharedPreferencesModule.kt @@ -0,0 +1,23 @@ +package campus.tech.kakao.map.repository.kakaomap + +import android.content.Context +import campus.tech.kakao.map.view.ActivityKeys +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 + +@Module +@InstallIn(SingletonComponent::class) +object SharedPreferencesModule { + + @Provides + @Singleton + fun provideSharedPreferences(@ApplicationContext context: Context) = + context.getSharedPreferences( + ActivityKeys.PREFS, + Context.MODE_PRIVATE + ) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/search/DBModule.kt b/app/src/main/java/campus/tech/kakao/map/repository/search/DBModule.kt new file mode 100644 index 00000000..805b77f0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/search/DBModule.kt @@ -0,0 +1,27 @@ +package campus.tech.kakao.map.repository.search + +import android.content.Context +import androidx.room.Room +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 + +@Module +@InstallIn(SingletonComponent::class) +object DBModule { + @Provides + @Singleton + fun provideDB(@ApplicationContext context: Context) = + Room.databaseBuilder( + context, + SearchKeywordDB::class.java, + "searchKeyword" + ).build() + + @Provides + fun provideDao(db: SearchKeywordDB) = + db.searchKeywordDao() +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/search/KakaoAPISetting.kt b/app/src/main/java/campus/tech/kakao/map/repository/search/KakaoAPISetting.kt new file mode 100644 index 00000000..477efc2b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/search/KakaoAPISetting.kt @@ -0,0 +1,20 @@ +package campus.tech.kakao.map.repository.search + +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.model.search.SearchResults +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +object KakaoAPISetting { + const val BASE_URL = "https://dapi.kakao.com" + const val API_KEY = "KakaoAK ${BuildConfig.KAKAO_REST_API_KEY}" +} + +interface KakaoSearchKeywordAPI { + @GET("/v2/local/search/keyword.json") + suspend fun getSearchKeyWord( + @Header("Authorization") key: String, + @Query("query") keyword: String + ): SearchResults +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/search/RetrofitModule.kt b/app/src/main/java/campus/tech/kakao/map/repository/search/RetrofitModule.kt new file mode 100644 index 00000000..b37dfcc4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/search/RetrofitModule.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.repository.search + +import campus.tech.kakao.map.repository.search.KakaoAPISetting.BASE_URL +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitModule { + + @Provides + @Singleton + fun provideRetrofitKakaoSearchAPI() = + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(KakaoSearchKeywordAPI::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/search/SavedSearchKeywordRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/search/SavedSearchKeywordRepository.kt new file mode 100644 index 00000000..0a04b773 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/search/SavedSearchKeywordRepository.kt @@ -0,0 +1,30 @@ +package campus.tech.kakao.map.repository.search + +import android.content.Context +import campus.tech.kakao.map.model.search.SearchKeyword +import campus.tech.kakao.map.model.search.SearchKeywordDao +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class SavedSearchKeywordRepository @Inject constructor(private val db: SearchKeywordDao) { + + suspend fun saveSearchKeyword(searchKeyword: SearchKeyword) { + withContext(Dispatchers.IO) { + db.insert(searchKeyword) + } + } + + suspend fun getSavedSearchKeywords(): List { + return withContext(Dispatchers.IO) { + db.getAll() + } + } + + suspend fun delSavedSearchKeyword(searchKeyword: SearchKeyword) { + withContext(Dispatchers.IO) { + db.delete(searchKeyword) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/search/SearchKeywordDB.kt b/app/src/main/java/campus/tech/kakao/map/repository/search/SearchKeywordDB.kt new file mode 100644 index 00000000..e57c5428 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/search/SearchKeywordDB.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map.repository.search + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import campus.tech.kakao.map.model.search.SearchKeyword +import campus.tech.kakao.map.model.search.SearchKeywordDao +import kotlinx.coroutines.coroutineScope + +@Database(entities = [SearchKeyword::class], version = 1) +abstract class SearchKeywordDB : RoomDatabase() { + abstract fun searchKeywordDao(): SearchKeywordDao +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/search/SearchRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/search/SearchRepository.kt new file mode 100644 index 00000000..ca4cd0cc --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/search/SearchRepository.kt @@ -0,0 +1,22 @@ +package campus.tech.kakao.map.repository.search + +import campus.tech.kakao.map.model.search.Place +import campus.tech.kakao.map.model.search.SearchKeyword +import campus.tech.kakao.map.repository.search.KakaoAPISetting.API_KEY +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class SearchRepository @Inject constructor(private val retrofitKakaoSearchKeyword: KakaoSearchKeywordAPI) { + + suspend fun search(searchKeyword: SearchKeyword): List { + if (searchKeyword.searchKeyword == "") + return emptyList() + + return withContext(Dispatchers.IO) { + retrofitKakaoSearchKeyword + .getSearchKeyWord(API_KEY, searchKeyword.searchKeyword) + .places + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/ActivityKeys.kt b/app/src/main/java/campus/tech/kakao/map/view/ActivityKeys.kt new file mode 100644 index 00000000..998771fb --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/ActivityKeys.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.view + +object ActivityKeys { + const val INTENT_PLACE = "place" + const val PREFS = "shared_preferences" + const val PREFS_PLACE = "place" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/MainActivity.kt new file mode 100644 index 00000000..e3f9887e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/MainActivity.kt @@ -0,0 +1,17 @@ +package campus.tech.kakao.map.view + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.R +import campus.tech.kakao.map.view.kakaomap.KakaoMapActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + val intent = Intent(this, KakaoMapActivity::class.java) + startActivity(intent) + finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/kakaomap/KakaoMapActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/kakaomap/KakaoMapActivity.kt new file mode 100644 index 00000000..a9513359 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/kakaomap/KakaoMapActivity.kt @@ -0,0 +1,166 @@ +package campus.tech.kakao.map.view.kakaomap + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivityKakaoMapBinding +import campus.tech.kakao.map.model.search.Place +import campus.tech.kakao.map.view.ActivityKeys +import campus.tech.kakao.map.view.search.SearchWindowActivity +import campus.tech.kakao.map.viewmodel.kakaomap.KakaoMapViewModel +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.kakao.vectormap.KakaoMap +import com.kakao.vectormap.KakaoMapReadyCallback +import com.kakao.vectormap.KakaoMapSdk +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 java.lang.Exception + +@AndroidEntryPoint +class KakaoMapActivity : AppCompatActivity() { + + private lateinit var binding: ActivityKakaoMapBinding + private val viewModel: KakaoMapViewModel by viewModels() + private lateinit var activityResultLauncher: ActivityResultLauncher + private lateinit var kakaoMap: KakaoMap + private lateinit var mapView: MapView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_kakao_map) + + setUpKakaoMap() + getSearchResult() + gotoSearchWindowBtnListener() + kakaoMapReloadListener() + } + + override fun onResume() { + super.onResume() + mapView.resume() + } + + override fun onPause() { + super.onPause() + mapView.pause() + } + + override fun onDestroy() { + super.onDestroy() + + val position = kakaoMap.cameraPosition?.position + if (position != null) { + viewModel.saveLastPosition(position) + } + } + + fun setUpKakaoMap() { + KakaoMapSdk.init(this, BuildConfig.KAKAO_API_KEY) + mapView = binding.mapView + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + + } + + override fun onMapError(error: Exception?) { + binding.errMsg = error?.message.toString() + showView(binding.kakaomapErr, true) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + this@KakaoMapActivity.kakaoMap = kakaoMap + + val position = viewModel.lastPosition.value + if (position != null) { + moveCameraPosition(position) + } + } + }) + } + + fun kakaoMapReloadListener() { + binding.reload.setOnClickListener { + showView(binding.kakaomapErr, false) + setUpKakaoMap() + } + } + + fun getSearchResult() { + activityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + val place = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + it.data?.getParcelableExtra( + ActivityKeys.INTENT_PLACE, + Place::class.java + ) + } else { + it.data?.getParcelableExtra(ActivityKeys.INTENT_PLACE) + } + if (place != null) { + displayPlaceOnKakaoMap(place) + } + } + } + } + + fun displayPlaceOnKakaoMap(place: Place) { + val position = LatLng.from(place.getLat(), place.getLng()) + displayPlaceInMarker(position) + moveCameraPosition(position) + displayPlaceInfoBottomSheet(place) + } + + fun displayPlaceInMarker(position: LatLng) { + kakaoMap.labelManager?.clearAll() + + val newStyles = LabelStyles.from(LabelStyle.from(R.drawable.marker)) + + val labelStyles = kakaoMap.labelManager?.addLabelStyles(newStyles) + + val options = LabelOptions + .from(position) + .setStyles(labelStyles) + + kakaoMap.labelManager?.layer?.addLabel(options) + } + + fun moveCameraPosition(position: LatLng) { + val cameraUpdate = CameraUpdateFactory.newCenterPosition(position) + kakaoMap.moveCamera(cameraUpdate) + } + + fun displayPlaceInfoBottomSheet(place: Place) { + val bottomSheet = binding.placeInfoBottomSheet + bottomSheet.place = place + + val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet.root) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + fun gotoSearchWindowBtnListener() { + binding.gotoSearchWindow.setOnClickListener { + val intent = Intent(this, SearchWindowActivity::class.java) + activityResultLauncher.launch(intent) + } + } + + private fun showView(view: View, isShow: Boolean) { + view.visibility = if (isShow) View.VISIBLE else View.GONE + } +} + diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/SavedSearchKeywordsAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/search/SavedSearchKeywordsAdapter.kt new file mode 100644 index 00000000..b22c1fd1 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/search/SavedSearchKeywordsAdapter.kt @@ -0,0 +1,59 @@ +package campus.tech.kakao.map.view.search + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.SavedSearchKeywordItemBinding +import campus.tech.kakao.map.model.search.SearchKeyword + +class SavedSearchKeywordsAdapter( + private val savedSearchKeywords: List, + private val layoutInflater: LayoutInflater +) : RecyclerView.Adapter() { + + private lateinit var itemClickListener: OnItemClickListener + + inner class ViewHolder(private val binding: SavedSearchKeywordItemBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: SearchKeyword) { + binding.searchKeyword = item + + binding.SavedSearchKeyword.setOnClickListener { + itemClickListener.onClickSavedSearchKeyword(item) + } + binding.delSavedSearchKeyword.setOnClickListener { + itemClickListener.onClickDelSavedSearchKeyword(item) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view: SavedSearchKeywordItemBinding = DataBindingUtil.inflate( + layoutInflater, + R.layout.saved_search_keyword_item, + parent, + false + ) + return ViewHolder(view) + } + + override fun getItemCount(): Int { + return savedSearchKeywords.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = savedSearchKeywords[position] + holder.bind(item) + } + + interface OnItemClickListener { + fun onClickSavedSearchKeyword(item: SearchKeyword) + fun onClickDelSavedSearchKeyword(item: SearchKeyword) + } + + fun setItemClickListener(onItemClickListener: OnItemClickListener) { + this.itemClickListener = onItemClickListener + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/SearchResultsAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/search/SearchResultsAdapter.kt new file mode 100644 index 00000000..849d0b3b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/search/SearchResultsAdapter.kt @@ -0,0 +1,54 @@ +package campus.tech.kakao.map.view.search + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.SearchResultItemBinding +import campus.tech.kakao.map.model.search.Place + +class SearchResultsAdapter( + private val searchResults: List, + private val layoutInflater: LayoutInflater +) : RecyclerView.Adapter() { + + private lateinit var itemClickListener: OnItemClickListener + + inner class ViewHolder(private val binding: SearchResultItemBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: Place) { + binding.place = item + binding.root.setOnClickListener { + itemClickListener.onClick(item) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view: SearchResultItemBinding = + DataBindingUtil.inflate( + layoutInflater, + R.layout.search_result_item, + parent, + false + ) + return ViewHolder(view) + } + + override fun getItemCount(): Int { + return searchResults.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(searchResults[position]) + } + + interface OnItemClickListener { + fun onClick(item: Place) + } + + fun setItemClickListener(onItemClickListener: OnItemClickListener) { + this.itemClickListener = onItemClickListener + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/SearchWindowActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/search/SearchWindowActivity.kt new file mode 100644 index 00000000..33b9e984 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/search/SearchWindowActivity.kt @@ -0,0 +1,127 @@ +package campus.tech.kakao.map.view.search + +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.widget.EditText +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.BindingAdapter +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.search.SearchKeyword +import campus.tech.kakao.map.databinding.ActivitySearchWindowBinding +import campus.tech.kakao.map.model.search.Place +import campus.tech.kakao.map.view.ActivityKeys +import campus.tech.kakao.map.viewmodel.search.SearchViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SearchWindowActivity : AppCompatActivity() { + + private lateinit var binding: ActivitySearchWindowBinding + private val viewModel: SearchViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_search_window) + + delSearchKeywordListener() + detectSearchWindowChangedListener() + displaySearchResults() + displaySavedSearchKeywords() + } + + private fun delSearchKeywordListener() { + binding.delSearchKeyword.setOnClickListener { + binding.searchKeyword = SearchKeyword("") + } + } + + private fun detectSearchWindowChangedListener() { + binding.searchWindow.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val searchKeyWord = SearchKeyword(s.toString()) + viewModel.getSearchResults(searchKeyWord) + } + + override fun afterTextChanged(s: Editable?) { + + } + }) + } + + private fun displaySearchResults() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.searchResults.collect { + if (it.isEmpty()) { + showView(binding.emptySearchResults, true) + showView(binding.searchResultsList, false) + } else { + showView(binding.emptySearchResults, false) + showView(binding.searchResultsList, true) + + val adapter = SearchResultsAdapter(it, layoutInflater) + adapter.setItemClickListener(object : + SearchResultsAdapter.OnItemClickListener { + override fun onClick(item: Place) { + val searchKeyword = SearchKeyword(item.place_name) + viewModel.saveSearchKeyword(searchKeyword) + + val intent = Intent() + intent.putExtra(ActivityKeys.INTENT_PLACE, item) + setResult(RESULT_OK, intent) + finish() + } + }) + + binding.searchResultsList.adapter = adapter + binding.searchResultsList.layoutManager = + LinearLayoutManager(this@SearchWindowActivity) + } + } + } + } + } + + private fun displaySavedSearchKeywords() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.savedSearchKeywords.collect { + + val adapter = SavedSearchKeywordsAdapter(it, layoutInflater) + adapter.setItemClickListener(object : + SavedSearchKeywordsAdapter.OnItemClickListener { + override fun onClickSavedSearchKeyword(item: SearchKeyword) { + binding.searchKeyword = item + } + + override fun onClickDelSavedSearchKeyword(item: SearchKeyword) { + viewModel.delSavedSearchKeyword(item) + } + }) + + binding.savedSearchKeywordsList.adapter = adapter + binding.savedSearchKeywordsList.layoutManager = LinearLayoutManager( + this@SearchWindowActivity, LinearLayoutManager.HORIZONTAL, false + ) + } + } + } + } + + private fun showView(view: View, isShow: Boolean) { + view.visibility = if (isShow) View.VISIBLE else View.GONE + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/kakaomap/KakaoMapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/kakaomap/KakaoMapViewModel.kt new file mode 100644 index 00000000..5acc3474 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/kakaomap/KakaoMapViewModel.kt @@ -0,0 +1,29 @@ +package campus.tech.kakao.map.viewmodel.kakaomap + +import androidx.lifecycle.ViewModel +import campus.tech.kakao.map.repository.kakaomap.LastPositionRepository +import com.kakao.vectormap.LatLng +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@HiltViewModel +class KakaoMapViewModel @Inject constructor( + private var lastPositionRepository: LastPositionRepository +) : ViewModel() { + private val _lastPosition = MutableStateFlow(null) + val lastPosition: StateFlow get() = _lastPosition + + init { + loadLastPosition() + } + + fun saveLastPosition(position: LatLng) { + lastPositionRepository.saveLastPosition(position) + } + + fun loadLastPosition() { + _lastPosition.value = lastPositionRepository.loadLastPosition() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/search/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/search/SearchViewModel.kt new file mode 100644 index 00000000..ae8ea5d3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/search/SearchViewModel.kt @@ -0,0 +1,55 @@ +package campus.tech.kakao.map.viewmodel.search + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.model.search.Place +import campus.tech.kakao.map.repository.search.SavedSearchKeywordRepository +import campus.tech.kakao.map.model.search.SearchKeyword +import campus.tech.kakao.map.repository.search.SearchRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private var searchRepository: SearchRepository, + private var savedSearchKeywordRepository: SavedSearchKeywordRepository +) : ViewModel() { + + private val _searchResults = MutableStateFlow>(emptyList()) + val searchResults: StateFlow> get() = _searchResults + private val _savedSearchKeywords = MutableStateFlow>(emptyList()) + val savedSearchKeywords: StateFlow> get() = _savedSearchKeywords + + init { + getSavedSearchKeywords() + } + + fun getSearchResults(searchKeyword: SearchKeyword) { + viewModelScope.launch { + _searchResults.value = searchRepository.search(searchKeyword) + } + } + + fun saveSearchKeyword(searchKeyword: SearchKeyword) { + viewModelScope.launch { + savedSearchKeywordRepository.saveSearchKeyword(searchKeyword) + getSavedSearchKeywords() + } + } + + fun getSavedSearchKeywords() { + viewModelScope.launch { + _savedSearchKeywords.value = savedSearchKeywordRepository.getSavedSearchKeywords() + } + } + + fun delSavedSearchKeyword(searchKeyword: SearchKeyword) { + viewModelScope.launch { + savedSearchKeywordRepository.delSavedSearchKeyword(searchKeyword) + getSavedSearchKeywords() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_custom_magnifying_glass.xml b/app/src/main/res/drawable/ic_custom_magnifying_glass.xml new file mode 100644 index 00000000..38508916 --- /dev/null +++ b/app/src/main/res/drawable/ic_custom_magnifying_glass.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_custom_reload.xml b/app/src/main/res/drawable/ic_custom_reload.xml new file mode 100644 index 00000000..de77f45a --- /dev/null +++ b/app/src/main/res/drawable/ic_custom_reload.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/marker.png b/app/src/main/res/drawable/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..229252b11bc57753651c3490f956318ead39c6a2 GIT binary patch literal 4115 zcmV+u5bW=XP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!T%Xm$brHmBv9lJcRp$pm6~A_fiTDT<$sio{JnmKj!f!^uv4n&OcC!_mqnd zR6Y9ST4_hBiE)==U+l!me9955G&Z;m2)Fe)TSKw9wO;? zIHU2RoGvzsP7M9VUb+N7<&Uu zHOc8@WY>!;s6F$%6NN*;i7{GIid*SNYSHcs(+CL-#0Qmew+k(5E2gO_*k;ZZ00kpP zFGh*(!|gO%lZ{gsO)0vE3jle>T0Vac>-gLa&RG22JCQ63QZ?>940&s@NL4c*X{8`c zxLZuf^NMNPNgeM(qcaANcqfuW;>6IOe3|CpQAS+Hpv@O%5kgJ{KQy%J#CSCZbG;ME zAW>r6IT(y8JVhses39fpyUF#$QOmX~q!ROqXq?(9*P_ocxlaxgohJOw$P++f#2E8c zgFjJIj*5*GA;7N~Q%Uqyy4QHDHXeJas4{n|;y<D3X;4L0+CMnxghewR=&$FfU z2z}U_p9k~SM{9+X_`PA&+{*?KLp%)B&r;zaJPZi`W~ahGLzJXB}g=u&!>B^MRnl>H5MDZ z6Y(H+Vo1s)H2`;+93_l>m*3owCYI9!)tDoa0}TGSsqMChHf zg8W88>2|!R8t`?cjM0n*u@b{pc?663H(sX*(fQzE#CC?4Y%HhyLce=qByc89*q=kS|-!^(Jt&fsi6v zB16I57+EY*1kQN8!{8`@H>{CkE3d%FMgzaV3OPtq~K10gs zGy#u!CyED}&zxF{?~|Nm;y0`z!v(;#?riat(GZy!cM%3rTPB+^KpVp-QJ1F5cjBBR z@_?{VOVFoUF^XpCIa9sTfP#X=P;2&QSU%a8ghpgyR4=AcTPm%BSs_qD;x0^a#$u(B zr3h*gW3+CZ%Rpqk$)#+_5p%9Eok_O*GBhF)<35BMKJ$2!Ne6`#QbOW(%%`T@?FrKd zHkhp>r-7!{HQhQvAzD+5LAsV9cmzb#n0A~^ZP~~4Lj{F6`*v((%bMy-QVWvv5})%% ziMXsFy$OA|&^ri+Xv8Rtu?h^ugvrDQ%{*mds>d(?gS~MB8`8VR^ah2Pvr-H<32aD& zCPpvDfvo70%|ir*^eyec^G+j{`jXVc`HmPTTxn&_*jQB+>(<5DzQh{giRs2^G-G9f zI{`&o^bis!*KBuqspGZW%n$~5Pgl6cQOcP z$`lR?tStgYDB6u@oT8Xe&3K=tNnU~q22W%5)rT<~vP4)@c0gca#J_a%5gokkOEL>H zbgWp8pu~9TK@C6m^(BE3ju^!j5R@3XgT^PSU*Se16NSe5z_lhpwoJED7-)_yU`>q7 zLFNss%}s+q?w}ZEuFXgn%Bcd8 zS%<;^t7?|=yH?}72H{$=W`q^=mtODlK;6Xx?FrBr6xdq|fjjvVg{__-IKmO5m}muc z{}RiWpq9T0)ytYTu&xgdOpMg7i&j+HcSVKOn05Z%?jePcaSSK?XkJ?2E<%4=rb7c8 zCMKfPmW}ExeBw(2BP=nmn}ML9wpPBUagukCU8r6h$wYa78)AsH{&R&&WP*!oW~+Hd1yT)gZZ*dY`#PBT8@$C|+TtW4=C!{K@Xj`bzkgo`u% zU}pCaQe-}a#tY`y5@9t)ZuneaVnmU>>2D3@-weWCNZ(R|;ejWg$V}6n_)zuXHD6+d z(8RDoJmHUP2ZKxsbkM|nrrHx^41JiWs!2^yGbW2SC_JmiSB}3 z=x0l7_ngm#R8^%j&&MHd39#I#r54`jSwHX37ZFgZZ?lxxnM#Qb2hv zt_DZwYTWAy(+77B2`R_T#)R3x8%q9d)YWRE6EhB*`3MhKwS6I{jURQWcUy3aCrl6A z`8bB*$zmnrNI^}Qz;pWB`5AfPxyZyADBPxHz}5C@?fhvCE~Ux%nJ+0i3@H_Og64LB z$v}ddFiE1l?wYyI`SzWbpGGXiI8Ati(b;nA7kngH^WOHI={|rv^))dyl#tLZ7(r5= zrGMDp1SvsD5lBz+4wDf4$`eFGd=*iGpIe2_c?db8IaQdfkHShl4+nZD3JdpsT*824 zg>sm$*o4M;NTck<0yfh3JwY_YMvN?n%h368YpoH884#aSjuE;7uP}FL_D&=ZcPLSg0lZAe^<4&g;Q+!Ee#7c~S&U29(fci)Um8TNT zqs7)5%eZ%!dJ|8_F*nV95MQCmY34mHRFxQP`oF@M;5?)ed5cIK7UvqxNf0xHvHY97 zaKC7gJRycw!f+;~x`zPkPEwi_9D~@AH(JKi)8sq7fvd%DCyEeP5pk zS$SzYuBUIg+Ly$F*ohI4oL{Pem|=Q~;<$rcLrk0`Z}b5qX*2iM(bUN%bBX`S<4)?6 z4C2Zxu7TVJQ8tv@k=%xuE_*SR;zOb~m^>uQ@5UzH`S7^k!cK%3{d?N)PQ1-rz-|qI zAdn}PUIrO%$PaBxg^Q8PmnI+5h)nveBuR{H1V00^q)8jbsiyeXRZD~z0h7iF%ui;A zT4E{-pbuo;vWh0`YrZ5_B$m+0(zm*e5tTS`Y0-NUq}D_W9ZWOF2_P|I1XRbVTB7@) z9h_N!G%{HWrxSNDcU$cV<43Z@G~%yxwzJH5<({H7vR*+uHmM5C^d*TPQDS^tsayT! zrLqq3$P#q);1X4DR+LT*i4!BM=J8{)DO6&iMgfpFVQpB-G9|tiP9Zb~P9s+89Ojs{ zCbMeN#Dwj@C#t+K%Xt-q7=g-R23y-kGiH%Bh;8&Vqs^kzY4JB+c7GQm$OXQnC@3m10y@?gX)b=gY4@-C)(!r~z{mfdYZ3;j()Y{SvEkYVCpY*#rPuO{F}n@oRK9` z1ewzqfjgf8bU!?!s&Jxt&EvC5A-RhD^@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..9e4c3859 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,9 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_place_info_bottom_sheet.xml b/app/src/main/res/layout/activity_place_info_bottom_sheet.xml new file mode 100644 index 00000000..32c44976 --- /dev/null +++ b/app/src/main/res/layout/activity_place_info_bottom_sheet.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_window.xml b/app/src/main/res/layout/activity_search_window.xml new file mode 100644 index 00000000..846c323e --- /dev/null +++ b/app/src/main/res/layout/activity_search_window.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/saved_search_keyword_item.xml b/app/src/main/res/layout/saved_search_keyword_item.xml new file mode 100644 index 00000000..e7fc1d8c --- /dev/null +++ b/app/src/main/res/layout/saved_search_keyword_item.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_result_item.xml b/app/src/main/res/layout/search_result_item.xml new file mode 100644 index 00000000..2019eb1b --- /dev/null +++ b/app/src/main/res/layout/search_result_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..1ceba7da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ Map + 검색어를 입력해 주세요. + 검색 결과가 없습니다. + 지도 인증을 실패했습니다.\n다시 시도해주세요. \ No newline at end of file diff --git a/app/src/test/java/campus/tech/kakao/map/KakaoMapViewModelTest.kt b/app/src/test/java/campus/tech/kakao/map/KakaoMapViewModelTest.kt new file mode 100644 index 00000000..e9f880d6 --- /dev/null +++ b/app/src/test/java/campus/tech/kakao/map/KakaoMapViewModelTest.kt @@ -0,0 +1,52 @@ +package campus.tech.kakao.map + +import campus.tech.kakao.map.repository.kakaomap.LastPositionRepository +import campus.tech.kakao.map.viewmodel.kakaomap.KakaoMapViewModel +import com.kakao.vectormap.LatLng +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Rule +import org.junit.Test + +private const val TEST_X = 36.37003 +private const val TEST_Y = 127.34594 + +@ExperimentalCoroutinesApi +class KakaoMapViewModelTest { + + @get:Rule + val mockkRule = MockKRule(this) + + private val lastPositionRepository = mockk(relaxed = true) + private val kakaoMapViewModel = KakaoMapViewModel(lastPositionRepository) + + @Test + fun testSaveLastLocation() { + // given + val position = LatLng.from(TEST_X, TEST_Y) + + // when + kakaoMapViewModel.saveLastPosition(position) + + // then + verify { lastPositionRepository.saveLastPosition(position) } + } + + @Test + fun testLoadLastLocation() { + // given + val position = LatLng.from(TEST_X, TEST_Y) + coEvery { lastPositionRepository.loadLastPosition() } returns position + + // when + kakaoMapViewModel.loadLastPosition() + + // then + assert(kakaoMapViewModel.lastPosition.value == position) + coVerify { lastPositionRepository.loadLastPosition() } + } +} \ No newline at end of file diff --git a/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt b/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt new file mode 100644 index 00000000..c49cc61c --- /dev/null +++ b/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt @@ -0,0 +1,112 @@ +package campus.tech.kakao.map + +import campus.tech.kakao.map.model.search.Place +import campus.tech.kakao.map.model.search.SearchKeyword +import campus.tech.kakao.map.repository.search.SavedSearchKeywordRepository +import campus.tech.kakao.map.repository.search.SearchRepository +import campus.tech.kakao.map.viewmodel.search.SearchViewModel +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.junit4.MockKRule +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +private const val TEST_SEARCH_KEYWORD = "TEST_SEARCH_KEYWORD" +private const val TEST_PLACE_NAME = "TEST_PLACE_NAME" +private const val TEST_PLACE_CATEGORY = "TEST_PLACE_CATEGORY" +private const val TEST_PLACE_ADDRESS = "TEST_PLACE_ADDRESS" +private const val TEST_PLACE_X = "36.37003" +private const val TEST_PLACE_Y = "127.34594" + +@ExperimentalCoroutinesApi +class SearchViewModelTest { + + @get:Rule + val mockkRule = MockKRule(this) + + private val searchRepository = mockk(relaxed = true) + private val savedSearchKeywordRepository = mockk(relaxed = true) + private val searchViewModel = SearchViewModel(searchRepository, savedSearchKeywordRepository) + + @Before + fun setUp() { + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @After + fun a() { + Dispatchers.resetMain() + } + + @Test + fun testGetSearchResults() { + // given + val searchKeyword = SearchKeyword(TEST_SEARCH_KEYWORD) + val place = Place( + TEST_PLACE_NAME, TEST_PLACE_CATEGORY, TEST_PLACE_ADDRESS, TEST_PLACE_X, + TEST_PLACE_Y + ) + val searchResults = listOf(place) + coEvery { searchRepository.search(searchKeyword) } returns searchResults + + // when + searchViewModel.getSearchResults(searchKeyword) + + // then + assert(searchViewModel.searchResults.value == searchResults) + coVerify { searchRepository.search(searchKeyword) } + + } + + @Test + fun testSaveSearchKeyword() { + // given + val searchKeyword = SearchKeyword(TEST_SEARCH_KEYWORD) + val savedSearchKeywords = listOf(searchKeyword) + coEvery { savedSearchKeywordRepository.getSavedSearchKeywords() } returns savedSearchKeywords + + //when + searchViewModel.saveSearchKeyword(searchKeyword) + + //then + assert(searchViewModel.savedSearchKeywords.value == savedSearchKeywords) + coVerify { savedSearchKeywordRepository.saveSearchKeyword(searchKeyword) } + } + + @Test + fun testGetSavedSearchKeywords() { + // given + val searchKeyword = SearchKeyword(TEST_SEARCH_KEYWORD) + val savedSearchKeywords = listOf(searchKeyword) + coEvery { savedSearchKeywordRepository.getSavedSearchKeywords() } returns savedSearchKeywords + + // when + searchViewModel.getSavedSearchKeywords() + + // then + assert(searchViewModel.savedSearchKeywords.value == savedSearchKeywords) + coVerify { savedSearchKeywordRepository.getSavedSearchKeywords() } + } + + @Test + fun testDelSavedSearchKeyword() { + // given + val searchKeyword = SearchKeyword(TEST_SEARCH_KEYWORD) + val savedSearchKeywords = emptyList() + coEvery { savedSearchKeywordRepository.getSavedSearchKeywords() } returns savedSearchKeywords + + //when + searchViewModel.delSavedSearchKeyword(searchKeyword) + + //then + assert(searchViewModel.savedSearchKeywords.value == savedSearchKeywords) + coVerify { savedSearchKeywordRepository.delSavedSearchKeyword(searchKeyword) } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 49f2a696..52260975 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false id("com.google.dagger.hilt.android") version "2.48.1" apply false - id("com.google.gms.google-services") version "4.4.2" apply false +// id("com.google.gms.google-services") version "4.4.2" apply false } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2927e499..3fa8f862 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Sat Jun 15 19:44:23 KST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists