From c9d8ff5fa651e51553bd060a70dff4d246d07f38 Mon Sep 17 00:00:00 2001 From: Jiye YU <93771689+YJY1220@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:08:17 +0900 Subject: [PATCH] =?UTF-8?q?[=EA=B2=BD=EB=B6=81=EB=8C=80=20Android=5F?= =?UTF-8?q?=EC=9C=A0=EC=A7=80=EC=98=88]=205=EC=A3=BC=EC=B0=A8=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C=5FStep=200=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs:readme.md * feat: step0 code upload with package, mocking --------- Co-authored-by: 유지예 --- README.md | 3 + app/build.gradle.kts | 74 ++--- .../campus/tech/kakao/map/ErrorScreenTest.kt | 47 +++ .../campus/tech/kakao/map/FunctionTest.kt | 178 ++++++++++++ .../example/map/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 16 +- .../java/campus/tech/kakao/MyApplication.kt | 12 + .../campus/tech/kakao/map/MainActivity.kt | 11 - .../kakao/map/model/KakaoSearchResponse.kt | 8 + .../campus/tech/kakao/map/model/MapItem.kt | 12 + .../tech/kakao/map/network/KakaoApiService.kt | 17 ++ .../kakao/map/network/RetrofitInstance.kt | 29 ++ .../tech/kakao/map/repository/MapAccess.kt | 32 ++ .../kakao/map/repository/MapRepository.kt | 17 ++ .../campus/tech/kakao/map/ui/MainActivity.kt | 274 ++++++++++++++++++ .../tech/kakao/map/ui/SearchActivity.kt | 148 ++++++++++ .../campus/tech/kakao/map/ui/SearchAdapter.kt | 53 ++++ .../kakao/map/ui/SearchBottomSheetFragment.kt | 56 ++++ .../tech/kakao/map/ui/SelectedAdapter.kt | 56 ++++ .../campus/tech/kakao/map/util/Constants.kt | 7 + .../tech/kakao/map/util/SQLiteHelpter.kt | 73 +++++ .../tech/kakao/map/viewmodel/MapViewModel.kt | 54 ++++ .../map/viewmodel/MapViewModelFactory.kt | 19 ++ app/src/main/res/drawable/ic_delete.xml | 5 + .../main/res/drawable/ic_location_marker.xml | 9 + app/src/main/res/drawable/ic_refresh.xml | 9 + app/src/main/res/drawable/new_marker.png | Bin 0 -> 1943 bytes .../main/res/drawable/search_background.xml | 4 + app/src/main/res/layout/activity_main.xml | 106 ++++++- app/src/main/res/layout/activity_search.xml | 62 ++++ .../layout/fragment_search_bottom_sheet.xml | 13 + .../main/res/layout/item_search_result.xml | 54 ++++ app/src/main/res/layout/item_selected.xml | 21 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 13 + .../java/com/example/map/ExampleUnitTest.kt | 17 ++ build.gradle.kts | 3 +- gradle/libs.versions.toml | 22 ++ settings.gradle.kts | 16 +- 42 files changed, 1520 insertions(+), 71 deletions(-) create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/ErrorScreenTest.kt create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt create mode 100644 app/src/androidTest/java/com/example/map/ExampleInstrumentedTest.kt create mode 100644 app/src/main/java/campus/tech/kakao/MyApplication.kt delete mode 100644 app/src/main/java/campus/tech/kakao/map/MainActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/KakaoSearchResponse.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/model/MapItem.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/network/KakaoApiService.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/network/RetrofitInstance.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/MapAccess.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/ui/SearchAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/ui/SearchBottomSheetFragment.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/ui/SelectedAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/util/Constants.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/util/SQLiteHelpter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt create mode 100644 app/src/main/res/drawable/ic_delete.xml create mode 100644 app/src/main/res/drawable/ic_location_marker.xml create mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/drawable/new_marker.png create mode 100644 app/src/main/res/drawable/search_background.xml create mode 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/layout/fragment_search_bottom_sheet.xml create mode 100644 app/src/main/res/layout/item_search_result.xml create mode 100644 app/src/main/res/layout/item_selected.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/example/map/ExampleUnitTest.kt create mode 100644 gradle/libs.versions.toml diff --git a/README.md b/README.md index 73ba54e1..8c399042 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ # android-map-refactoring +### KakaoTechCampus 2기 Step2 - 5주차 과제 : 리팩토링 + +### 0단계. 4주차 코드 반영하기 \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c1a15f2..ae9d3282 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,10 +1,14 @@ +import java.util.Properties + +fun getApiKey(key: String): String { + val properties = Properties() + file("../local.properties").inputStream().use { properties.load(it) } + return properties.getProperty(key) +} + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") - id("org.jlleitschuh.gradle.ktlint") - id("kotlin-parcelize") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") } android { @@ -19,6 +23,10 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + //API 가져오기 + resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY")) + buildConfigField("String", "KAKAO_REST_API_KEY", "\"${getApiKey("KAKAO_REST_API_KEY")}\"") } buildTypes { @@ -30,51 +38,51 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { jvmTarget = "17" } buildFeatures { + viewBinding = true dataBinding = true buildConfig = true } } dependencies { - - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("com.google.android.material:material:1.12.0") + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.recyclerview:recyclerview:1.3.2") - implementation("com.squareup.retrofit2:retrofit:2.11.0") - implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("androidx.datastore:datastore-preferences:1.0.0") + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.9.0") + implementation("com.kakao.sdk:v2-user:2.10.0") // Kakao SDK 추가 implementation("com.kakao.maps.open:android:2.9.5") - implementation("androidx.activity:activity-ktx:1.9.0") - implementation("androidx.test:core-ktx:1.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") - implementation("androidx.room:room-runtime:2.6.1") - kapt("androidx.room:room-compiler:2.6.1") - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - implementation("androidx.activity:activity-ktx:1.9.0") - implementation("androidx.room:room-ktx:2.6.1") - testImplementation("androidx.room:room-testing:2.6.1") + implementation("androidx.activity:activity:1.8.0") testImplementation("junit:junit:4.13.2") - testImplementation("io.mockk:mockk-android:1.13.11") - testImplementation("io.mockk:mockk-agent:1.13.11") - testImplementation("androidx.arch.core:core-testing:2.2.0") - testImplementation("org.robolectric:robolectric:4.11.1") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") - androidTestImplementation("androidx.test:rules:1.6.1") - androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1") -} + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("androidx.fragment:fragment-ktx:1.3.6") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") + implementation("com.google.android.gms:play-services-maps:19.0.0") + implementation("com.google.android.material:material:1.11.0") + testImplementation ("junit:junit:4.13.2") + androidTestImplementation ("androidx.test.ext:junit:1.1.3") + androidTestImplementation ("androidx.test.espresso:espresso-core:3.4.0") + androidTestImplementation ("androidx.test:core-ktx:1.4.0") + androidTestImplementation ("androidx.test:runner:1.4.0") + androidTestImplementation ("androidx.test:rules:1.4.0") + androidTestImplementation ("org.mockito:mockito-core:3.11.2") + androidTestImplementation ("org.mockito:mockito-android:3.11.2") + androidTestImplementation ("androidx.arch.core:core-testing:2.1.0") +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/ErrorScreenTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/ErrorScreenTest.kt new file mode 100644 index 00000000..6b1d0d26 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/ErrorScreenTest.kt @@ -0,0 +1,47 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.view.View +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import campus.tech.kakao.map.ui.MainActivity +import org.junit.* +import org.junit.runner.RunWith +import org.junit.Assert.assertTrue + +@RunWith(AndroidJUnit4::class) +class ErrorScreenTest { + + private lateinit var scenario: ActivityScenario + private lateinit var context: Context + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + scenario = ActivityScenario.launch(MainActivity::class.java) + } + + @After + fun tearDown() { + if (::scenario.isInitialized) { + scenario.close() + } + } + + @Test + fun testShowErrorScreen() { + scenario.onActivity { activity -> + // 에러 화면이 표시될 때까지 대기 + Thread.sleep(2000) + + val errorLayout: RelativeLayout = activity.findViewById(R.id.error_layout) + val errorDetails: TextView = activity.findViewById(R.id.error_details) + + assertTrue(errorLayout.visibility == View.VISIBLE) + assertTrue(errorDetails.text.toString().contains("Unauthorized")) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt new file mode 100644 index 00000000..f3acdc81 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt @@ -0,0 +1,178 @@ +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.Intent +import android.view.View +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import campus.tech.kakao.map.model.MapItem +import campus.tech.kakao.map.repository.MapRepository +import campus.tech.kakao.map.viewmodel.MapViewModel +import campus.tech.kakao.map.viewmodel.MapViewModelFactory +import campus.tech.kakao.map.R +import campus.tech.kakao.map.ui.MainActivity +import campus.tech.kakao.map.ui.SearchActivity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.* +import org.junit.runner.RunWith +import org.mockito.Mockito.* +import java.util.concurrent.Executors + +@RunWith(AndroidJUnit4::class) +class FunctionTest { + + private lateinit var scenarioMain: ActivityScenario + private lateinit var scenarioSearch: ActivityScenario + private lateinit var context: Context + private lateinit var repository: MapRepository + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + repository = mock(MapRepository::class.java) + + val application = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application + val viewModelFactory = MapViewModelFactory(application, repository) + + scenarioMain = ActivityScenario.launch(MainActivity::class.java) + scenarioMain.onActivity { activity -> + activity.viewModelFactory = viewModelFactory + } + } + + @After + fun tearDown() { + scenarioMain.close() + if (::scenarioSearch.isInitialized) { + scenarioSearch.close() + } + } + + @Test + fun testCompleteFlow() { + scenarioMain.onActivity { mainActivity -> + + val searchEditText: EditText = mainActivity.findViewById(R.id.search_edit_text) + searchEditText.performClick() + + Executors.newSingleThreadExecutor().execute { + scenarioSearch = ActivityScenario.launch(SearchActivity::class.java) + scenarioSearch.onActivity { searchActivity -> + + // Repository Mocking 설정 + runBlocking { + `when`(repository.searchItems("바다 정원")).thenReturn( + listOf( + MapItem( + "1", + "바다 정원", + "강원도 고성군", + "카페", + 127.0, + 37.0 + ) + ) + ) + } + + // ViewModel에 Mock Repository 주입 + searchActivity.viewModel = MapViewModel(searchActivity.application, repository) + + //1 검색 기능 테스트 + val searchEditText: EditText = searchActivity.findViewById(R.id.searchEditText) + searchEditText.setText("바다 정원") + searchActivity.performSearch("바다 정원") + + //2 recyclerview로 검색결과 보이는지 테스트 + val searchResultsRecyclerView: RecyclerView = + searchActivity.findViewById(R.id.searchResultsRecyclerView) + searchResultsRecyclerView.adapter?.notifyDataSetChanged() + assertEquals(1, searchResultsRecyclerView.adapter?.itemCount) + + //3. 해당 검색결과 중 하나 눌러서 지도 마커표시, bottomsheet 정보 표시 + searchResultsRecyclerView.findViewHolderForAdapterPosition(0)?.itemView?.performClick() + + searchActivity.setResultAndFinish( + MapItem( + "0", + "바다 정원", + "강원도 고성군", + "카페", + 127.0, + 37.0 + ) + ) + val resultIntent = Intent().apply { + putExtra("place_name", "바다 정원") + putExtra("road_address_name", "강원도 고성군") + putExtra("x", 127.0) + putExtra("y", 37.0) + } + + mainActivity.onActivityResult( + MainActivity.SEARCH_REQUEST_CODE, + Activity.RESULT_OK, + resultIntent + ) + val bottomSheetTitle: TextView = + mainActivity.findViewById(R.id.bottomSheetTitle) + val bottomSheetAddress: TextView = + mainActivity.findViewById(R.id.bottomSheetAddress) + assertEquals("바다 정원", bottomSheetTitle.text.toString()) + assertEquals("강원도 고성군", bottomSheetAddress.text.toString()) + + // 4. 마지막 위치 저장해서 다시 앱 실행시 해당 위치로 지도 뜨도록 하기 + mainActivity.saveLastMarkerPosition(37.0, 127.0, "바다 정원", "강원도 고성군") + } + + scenarioMain.recreate() + val onActivity = scenarioMain.onActivity { activity -> + activity.loadLastMarkerPosition() + + val bottomSheetTitle: TextView = activity.findViewById(R.id.bottomSheetTitle) + val bottomSheetAddress: TextView = + activity.findViewById(R.id.bottomSheetAddress) + assertEquals("바다 정원", bottomSheetTitle.text.toString()) + assertEquals("강원도 고성군", bottomSheetAddress.text.toString()) + assertEquals( + View.VISIBLE, + activity.findViewById(R.id.bottomSheetLayout).visibility + ) + + // 5. searchactivity에서 저장된 검색어 남아있는지 확인하고 해당 검색어 누르면 해당 검색어로 재검색되는지 확인하기 + val searchEditText: EditText = activity.findViewById(R.id.search_edit_text) + searchEditText.performClick() + + Executors.newSingleThreadExecutor().execute { + scenarioSearch = ActivityScenario.launch(SearchActivity::class.java) + scenarioSearch.onActivity { searchActivity -> + + val selectedItemsRecyclerView: RecyclerView = + searchActivity.findViewById(R.id.selectedItemsRecyclerView) + assertEquals(1, selectedItemsRecyclerView.adapter?.itemCount) + val selectedViewHolder = + selectedItemsRecyclerView.findViewHolderForAdapterPosition(0) + assertEquals( + "바다 정원", + selectedViewHolder?.itemView?.findViewById(R.id.selectedItemName)?.text.toString() + ) + + selectedViewHolder?.itemView?.performClick() + searchActivity.performSearch("바다 정원") + + val searchResultsRecyclerView: RecyclerView = + searchActivity.findViewById(R.id.searchResultsRecyclerView) + assertEquals(1, searchResultsRecyclerView.adapter?.itemCount) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/map/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/map/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..e143a6ac --- /dev/null +++ b/app/src/androidTest/java/com/example/map/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.map + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.map", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..d913983a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,11 @@ + + + + + + + @@ -21,6 +31,8 @@ + + - + \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/MyApplication.kt b/app/src/main/java/campus/tech/kakao/MyApplication.kt new file mode 100644 index 00000000..853f4ccf --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/MyApplication.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao + +import android.app.Application +import campus.tech.kakao.map.R +import com.kakao.vectormap.KakaoMapSdk + +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + KakaoMapSdk.init(this, getString(R.string.kakao_api_key)) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt deleted file mode 100644 index 95b43803..00000000 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package campus.tech.kakao.map - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/model/KakaoSearchResponse.kt b/app/src/main/java/campus/tech/kakao/map/model/KakaoSearchResponse.kt new file mode 100644 index 00000000..745ab072 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/KakaoSearchResponse.kt @@ -0,0 +1,8 @@ +package campus.tech.kakao.map.model + +import campus.tech.kakao.map.model.MapItem + +//MapItem 넘겨받기 -> 매핑 +data class KakaoSearchResponse( + val documents: List +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/MapItem.kt b/app/src/main/java/campus/tech/kakao/map/model/MapItem.kt new file mode 100644 index 00000000..28ab31cf --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/MapItem.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map.model + +//이름 보다 알기 쉽게 변경 - api맞춰서 +data class MapItem( + val id: String, + val place_name: String, + val road_address_name: String, + val category_group_name: String, + val x: Double, + val y: Double + +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/network/KakaoApiService.kt b/app/src/main/java/campus/tech/kakao/map/network/KakaoApiService.kt new file mode 100644 index 00000000..72a3d3f3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/network/KakaoApiService.kt @@ -0,0 +1,17 @@ +package campus.tech.kakao.map.network + +import campus.tech.kakao.map.model.KakaoSearchResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface KakaoApiService { + @GET("v2/local/search/keyword.json") + suspend fun searchPlaces( + @Header("Authorization") apiKey: String, + @Query("query") query: String, + @Query("page") page: Int = 1, + @Query("size") size: Int = 15 + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/network/RetrofitInstance.kt b/app/src/main/java/campus/tech/kakao/map/network/RetrofitInstance.kt new file mode 100644 index 00000000..49913831 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/network/RetrofitInstance.kt @@ -0,0 +1,29 @@ +package campus.tech.kakao.map.network + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor + +object RetrofitInstance { + + private const val BASE_URL = "https://dapi.kakao.com/" + + private val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + private val httpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + private val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(httpClient) + .build() + + val api: KakaoApiService by lazy { + retrofit.create(KakaoApiService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/MapAccess.kt b/app/src/main/java/campus/tech/kakao/map/repository/MapAccess.kt new file mode 100644 index 00000000..b53d2a05 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/MapAccess.kt @@ -0,0 +1,32 @@ +package campus.tech.kakao.map.repository + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import android.util.Log +import campus.tech.kakao.map.model.MapItem +import campus.tech.kakao.map.network.RetrofitInstance +import campus.tech.kakao.map.util.Constants + +class MapAccess(context: Context) { + + // 검색어 기반 항목 검색 suspend 함수 + suspend fun searchItems(query: String, page: Int = 1, size: Int = 15): List { + + return withContext(Dispatchers.IO) { + + val apiKey = Constants.KAKAO_API_KEY + + val response = RetrofitInstance.api.searchPlaces(apiKey, query, page, size) + + //응답여부 체크 + if (response.isSuccessful) { + Log.d("MapAccess", "Response: ${response.body()?.documents}") + response.body()?.documents ?: emptyList() + } else { + Log.e("MapAccess", "Error: ${response.errorBody()?.string()}") + emptyList() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt new file mode 100644 index 00000000..19addf1f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt @@ -0,0 +1,17 @@ +package campus.tech.kakao.map.repository + +import android.app.Application +import campus.tech.kakao.map.model.MapItem + +interface MapRepository { + suspend fun searchItems(query: String): List +} + +class MapRepositoryImpl(private val application: Application) : MapRepository { + + private val mapAccess = MapAccess(application) + + override suspend fun searchItems(query: String): List { + return mapAccess.searchItems(query) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt new file mode 100644 index 00000000..82c89a85 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt @@ -0,0 +1,274 @@ +package campus.tech.kakao.map.ui + + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log + +import android.widget.EditText +import androidx.appcompat.app.AppCompatActivity +import com.kakao.vectormap.MapView +import com.kakao.vectormap.MapLifeCycleCallback +import com.kakao.vectormap.KakaoMapReadyCallback +import com.kakao.vectormap.KakaoMap + +import android.view.View +import android.widget.ImageButton +import android.widget.RelativeLayout +import android.widget.TextView +import android.app.Activity +import android.widget.FrameLayout +import campus.tech.kakao.map.viewmodel.MapViewModelFactory +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.MapItem +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.kakao.vectormap.LatLng +import com.kakao.vectormap.label.LabelLayer +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles +import com.kakao.vectormap.label.LabelTextStyle +import com.kakao.vectormap.camera.CameraAnimation +import com.kakao.vectormap.camera.CameraUpdateFactory + + +class MainActivity : AppCompatActivity() { + + lateinit var viewModelFactory: MapViewModelFactory + private lateinit var mapView: MapView + private lateinit var errorLayout: RelativeLayout + private lateinit var errorMessage: TextView + private lateinit var errorDetails: TextView + private lateinit var retryButton: ImageButton + private lateinit var kakaoMap: KakaoMap + private lateinit var labelLayer: LabelLayer + private lateinit var bottomSheetBehavior: BottomSheetBehavior + private lateinit var bottomSheetTitle: TextView + private lateinit var bottomSheetAddress: TextView + private lateinit var bottomSheetLayout: FrameLayout + private var selectedItems = mutableListOf() + + companion object { + const val SEARCH_REQUEST_CODE = 1 + const val PREFS_NAME = "LastMarkerPrefs" + const val PREF_LATITUDE = "lastLatitude" + const val PREF_LONGITUDE = "lastLongitude" + const val PREF_PLACE_NAME = "lastPlaceName" + const val PREF_ROAD_ADDRESS_NAME = "lastRoadAddressName" + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // 카카오 지도 초기화 + mapView = findViewById(R.id.map_view) + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + // 지도 API가 정상적으로 종료될 때 호출됨 + } + + override fun onMapError(error: Exception) { + + showErrorScreen(error) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(map: KakaoMap) { + kakaoMap = map + labelLayer = kakaoMap.labelManager?.layer!! + // 마지막 마커 위치 불러오기 + loadLastMarkerPosition() + + } + }) + + // 검색창 클릭 시 검색 페이지로 이동 + val searchEditText = findViewById(R.id.search_edit_text) + searchEditText.setOnClickListener { + val intent = Intent(this, SearchActivity::class.java) + + intent.putExtra("selectedItemsSize", selectedItems.size) + selectedItems.forEachIndexed { index, mapItem -> + intent.putExtra("id_$index", mapItem.id) + intent.putExtra("place_name_$index", mapItem.place_name) + intent.putExtra("road_address_name_$index", mapItem.road_address_name) + intent.putExtra("category_group_name_$index", mapItem.category_group_name) + } + startActivityForResult(intent, SEARCH_REQUEST_CODE) + } + + // 에러 화면 초기화 + errorLayout = findViewById(R.id.error_layout) + errorMessage = findViewById(R.id.error_message) + errorDetails = findViewById(R.id.error_details) + retryButton = findViewById(R.id.retry_button) + + // BottomSheet 초기화 + bottomSheetLayout = findViewById(R.id.bottomSheetLayout) + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout) + bottomSheetTitle = findViewById(R.id.bottomSheetTitle) + bottomSheetAddress = findViewById(R.id.bottomSheetAddress) + + // 처음에는 BottomSheet 숨기기 + bottomSheetLayout.visibility = View.GONE + } + + // 지도 -> 검색페이지 돌아갈 때 저장된 검색어 목록 그대로 저장 + private fun processIntentData() { + val placeName = intent.getStringExtra("place_name") + val roadAddressName = intent.getStringExtra("road_address_name") + val x = intent.getDoubleExtra("x", 0.0) + val y = intent.getDoubleExtra("y", 0.0) + if (placeName != null && roadAddressName != null) { + addLabel(placeName, roadAddressName, x, y) + + } + } + + override fun onResume() { + super.onResume() + mapView.resume() // MapView의 resume 호출 + } + + override fun onPause() { + super.onPause() + mapView.pause() // MapView의 pause 호출 + + } + + fun showErrorScreen(error: Exception) { + errorLayout.visibility = View.VISIBLE + errorDetails.text = error.message + mapView.visibility = View.GONE + } + + fun onRetryButtonClick(view: View) { + errorLayout.visibility = View.GONE + mapView.visibility = View.VISIBLE + // 지도 다시 시작 + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + } + + override fun onMapError(error: Exception) { + showErrorScreen(error) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + this@MainActivity.kakaoMap = kakaoMap + labelLayer = kakaoMap.labelManager?.layer!! + loadLastMarkerPosition() // 마지막 마커 위치 불러오기 + } + }) + } + + // 결과 반환 + public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == SEARCH_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + data?.let { + val placeName = it.getStringExtra("place_name") + val roadAddressName = it.getStringExtra("road_address_name") + val x = it.getDoubleExtra("x", 0.0) + val y = it.getDoubleExtra("y", 0.0) + // 다시 돌아갈 때 저장된 검색어 확인 + selectedItems.clear() + val selectedItemsSize = it.getIntExtra("selectedItemsSize", 0) + for (i in 0 until selectedItemsSize) { + val id = it.getStringExtra("id_$i") ?: "" + val place_name = it.getStringExtra("place_name_$i") ?: "" + val road_address_name = it.getStringExtra("road_address_name_$i") ?: "" + val category_group_name = it.getStringExtra("category_group_name_$i") ?: "" + val x = it.getDoubleExtra("x_$i", 0.0) + val y = it.getDoubleExtra("y_$i", 0.0) + selectedItems.add(MapItem(id, place_name, road_address_name, category_group_name, x, y)) + } + + //마커 위치 저장 + addLabel(placeName, roadAddressName, x, y) + if (placeName != null && roadAddressName != null) { + saveLastMarkerPosition(x, y, placeName, roadAddressName) + } + } + } + } + + // label marker + private fun addLabel(placeName: String?, roadAddressName: String?, x: Double, y: Double) { + if (placeName != null && roadAddressName != null) { + val position = LatLng.from(y, x) + val styles = kakaoMap.labelManager?.addLabelStyles( + LabelStyles.from( + LabelStyle.from(R.drawable.new_marker).setZoomLevel(1), + LabelStyle.from(R.drawable.new_marker) + .setTextStyles(LabelTextStyle.from(this, R.style.labelTextStyle_1)) + .setZoomLevel(1) + ) + ) + + labelLayer.addLabel( + LabelOptions.from(placeName, position).setStyles(styles).setTexts(placeName) + ) + + // 카메라 이동 + moveCamera(position) + + // 마커 위치 저장 + saveLastMarkerPosition(x, y, placeName, roadAddressName) + + // bottom sheet 업데이트 + updateBottomSheet(placeName, roadAddressName) + } + } + + private fun moveCamera(position: LatLng) { + kakaoMap.moveCamera( + CameraUpdateFactory.newCenterPosition(position), + CameraAnimation.from(10, false, false) + ) + } + + public fun saveLastMarkerPosition(latitude: Double, longitude: Double, placeName: String, roadAddressName: String) { + val sharedPreferences = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + with(sharedPreferences.edit()) { + putFloat(PREF_LATITUDE, latitude.toFloat()) + putFloat(PREF_LONGITUDE, longitude.toFloat()) + putString(PREF_PLACE_NAME, placeName) + putString(PREF_ROAD_ADDRESS_NAME, roadAddressName) + apply() + } + } + + //마커 다시 로드하기 + fun loadLastMarkerPosition() { + val sharedPreferences = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + if (sharedPreferences.contains(PREF_LATITUDE) && sharedPreferences.contains(PREF_LONGITUDE)) { + val latitude = sharedPreferences.getFloat(PREF_LATITUDE, 0.0f).toDouble() + val longitude = sharedPreferences.getFloat(PREF_LONGITUDE, 0.0f).toDouble() + val placeName = sharedPreferences.getString(PREF_PLACE_NAME, "") ?: "" + val roadAddressName = sharedPreferences.getString(PREF_ROAD_ADDRESS_NAME, "") ?: "" + + if (placeName.isNotEmpty() && roadAddressName.isNotEmpty()) { + Log.d("MainActivity", "Loaded last marker position: lat=$latitude, lon=$longitude, placeName=$placeName, roadAddressName=$roadAddressName") + addLabel(placeName, roadAddressName, longitude, latitude) + val position = LatLng.from(latitude, longitude) + moveCamera(position) + updateBottomSheet(placeName, roadAddressName) + } else { + Log.d("MainActivity", "No place name or road address name found") + } + } else { + Log.d("MainActivity", "No last marker position found in SharedPreferences") + } + } + + //재실행시 bottomsheet 나타나는 거 방지 + private fun updateBottomSheet(placeName: String, roadAddressName: String) { + bottomSheetTitle.text = placeName + bottomSheetAddress.text = roadAddressName + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetLayout.visibility = View.VISIBLE + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt new file mode 100644 index 00000000..09c7fe77 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt @@ -0,0 +1,148 @@ +package campus.tech.kakao.map.ui + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import campus.tech.kakao.map.databinding.ActivitySearchBinding +import androidx.lifecycle.ViewModelProvider +import android.widget.Toast +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import android.app.Activity +import android.content.Intent +import campus.tech.kakao.map.viewmodel.MapViewModel +import campus.tech.kakao.map.viewmodel.MapViewModelFactory +import campus.tech.kakao.map.R +import campus.tech.kakao.map.util.SQLiteHelper +import campus.tech.kakao.map.model.MapItem +import campus.tech.kakao.map.repository.MapRepositoryImpl + +class SearchActivity : AppCompatActivity() { + + private lateinit var binding: ActivitySearchBinding + private lateinit var sqLiteHelper: SQLiteHelper + lateinit var viewModel: MapViewModel + private lateinit var searchAdapter: SearchAdapter + private lateinit var selectedAdapter: SelectedAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySearchBinding.inflate(layoutInflater) + setContentView(binding.root) + + sqLiteHelper = SQLiteHelper(this) + sqLiteHelper.writableDatabase + + val repository = MapRepositoryImpl(application) + val viewModelFactory = MapViewModelFactory(application, repository) + viewModel = ViewModelProvider(this, viewModelFactory).get(MapViewModel::class.java) + + setupRecyclerViews() + setupSearchEditText() + setupClearTextButton() + observeViewModel() + + val selectedItemsSize = intent.getIntExtra("selectedItemsSize", 0) + val selectedItems = mutableListOf() + for (i in 0 until selectedItemsSize) { + val id = intent.getStringExtra("id_$i") ?: "" + val place_name = intent.getStringExtra("place_name_$i") ?: "" + val road_address_name = intent.getStringExtra("road_address_name_$i") ?: "" + val category_group_name = intent.getStringExtra("category_group_name_$i") ?: "" + val x = intent.getDoubleExtra("x_$i", 0.0) + val y = intent.getDoubleExtra("y_$i", 0.0) + selectedItems.add(MapItem(id, place_name, road_address_name, category_group_name, x, y)) + } + viewModel.setSelectedItems(selectedItems) + } + + private fun setupRecyclerViews() { + searchAdapter = SearchAdapter { item -> + if (viewModel.selectedItems.value?.contains(item) == true) { + Toast.makeText(this, getString(R.string.item_already_selected), Toast.LENGTH_SHORT).show() + } else { + viewModel.selectItem(item) + setResultAndFinish(item) + } + } + + binding.searchResultsRecyclerView.apply { + layoutManager = LinearLayoutManager(this@SearchActivity) + adapter = searchAdapter + } + + selectedAdapter = SelectedAdapter( + onItemRemoved = { item -> viewModel.removeSelectedItem(item) }, + onItemClicked = { item -> performSearch(item.place_name) } + ) + + binding.selectedItemsRecyclerView.apply { + layoutManager = LinearLayoutManager(this@SearchActivity, RecyclerView.HORIZONTAL, false) + adapter = selectedAdapter + } + } + + private fun setupSearchEditText() { + binding.searchEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + if (s.toString().isNotEmpty()) { + binding.clearTextButton.visibility = View.VISIBLE + } else { + binding.clearTextButton.visibility = View.GONE + } + viewModel.searchQuery.value = s.toString() + } + + override fun afterTextChanged(s: Editable?) {} + }) + } + + private fun setupClearTextButton() { + binding.clearTextButton.setOnClickListener { + binding.searchEditText.text.clear() + } + } + + private fun observeViewModel() { + viewModel.searchResults.observe(this, Observer { results -> + searchAdapter.submitList(results) + + binding.noResultsTextView.visibility = if (results.isEmpty() && !viewModel.searchQuery.value.isNullOrEmpty()) View.VISIBLE else View.GONE + binding.searchResultsRecyclerView.visibility = if (results.isEmpty()) View.GONE else View.VISIBLE + }) + + viewModel.selectedItems.observe(this, Observer { selectedItems -> + selectedAdapter.submitList(selectedItems) + }) + } + + fun performSearch(query: String) { + binding.searchEditText.setText(query) + viewModel.searchQuery.value = query + } + + fun setResultAndFinish(selectedItem: MapItem) { + val intent = Intent().apply { + putExtra("place_name", selectedItem.place_name) + putExtra("road_address_name", selectedItem.road_address_name) + putExtra("x", selectedItem.x) + putExtra("y", selectedItem.y) + putExtra("selectedItemsSize", viewModel.selectedItems.value?.size ?: 0) + viewModel.selectedItems.value?.forEachIndexed { index, item -> + putExtra("id_$index", item.id) + putExtra("place_name_$index", item.place_name) + putExtra("road_address_name_$index", item.road_address_name) + putExtra("category_group_name_$index", item.category_group_name) + putExtra("x_$index", item.x) + putExtra("y_$index", item.y) + } + } + setResult(Activity.RESULT_OK, intent) + finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/SearchAdapter.kt b/app/src/main/java/campus/tech/kakao/map/ui/SearchAdapter.kt new file mode 100644 index 00000000..509f9cc3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/SearchAdapter.kt @@ -0,0 +1,53 @@ +package campus.tech.kakao.map.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.databinding.ItemSearchResultBinding +import campus.tech.kakao.map.model.MapItem + +class SearchAdapter( + private val onItemClicked: (MapItem) -> Unit // 아이템 클릭 시 호출되는 함수 +) : RecyclerView.Adapter() { + + private var items: List = emptyList() // 현재 목록을 저장하는 리스트 + + // 데이터를 갱신 + fun submitList(newItems: List) { + items = newItems + notifyDataSetChanged() // 전체 데이터 갱신 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder { + + val binding = ItemSearchResultBinding.inflate( + LayoutInflater.from(parent.context), // LayoutInflater 생성 + parent, + false + ) + return SearchViewHolder(binding) + } + + override fun onBindViewHolder(holder: SearchViewHolder, position: Int) { + val currentItem = items[position] // 지금 데이터 가져오기 + holder.bind(currentItem) + } + + // RecyclerView 데이터 개수 반환 + override fun getItemCount(): Int { + return items.size + } + + inner class SearchViewHolder(private val binding: ItemSearchResultBinding) : + RecyclerView.ViewHolder(binding.root) { // 루트 view 초기화 + // 데이터 view에 바인딩 + fun bind(item: MapItem) { + binding.apply { + itemName.text = item.place_name + itemAddress.text = item.road_address_name + itemCategory.text = item.category_group_name + root.setOnClickListener { onItemClicked(item) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/SearchBottomSheetFragment.kt b/app/src/main/java/campus/tech/kakao/map/ui/SearchBottomSheetFragment.kt new file mode 100644 index 00000000..6cb7495e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/SearchBottomSheetFragment.kt @@ -0,0 +1,56 @@ +package campus.tech.kakao.map.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import campus.tech.kakao.map.viewmodel.MapViewModel +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import campus.tech.kakao.map.databinding.FragmentSearchBottomSheetBinding + +class SearchBottomSheetFragment : BottomSheetDialogFragment() { + + private var _binding: FragmentSearchBottomSheetBinding? = null + private val binding get() = _binding!! + private val viewModel: MapViewModel by activityViewModels() + private lateinit var searchAdapter: SearchAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSearchBottomSheetBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + observeViewModel() + } + + private fun setupRecyclerView() { + searchAdapter = SearchAdapter { item -> + viewModel.selectItem(item) + dismiss() + } + + binding.searchResultsRecyclerView.apply { + layoutManager = LinearLayoutManager(context) + adapter = searchAdapter + } + } + + private fun observeViewModel() { + viewModel.searchResults.observe(viewLifecycleOwner, { results -> + searchAdapter.submitList(results) + }) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/SelectedAdapter.kt b/app/src/main/java/campus/tech/kakao/map/ui/SelectedAdapter.kt new file mode 100644 index 00000000..22b12efa --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/SelectedAdapter.kt @@ -0,0 +1,56 @@ +package campus.tech.kakao.map.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.databinding.ItemSelectedBinding +import campus.tech.kakao.map.model.MapItem + +class SelectedAdapter( + + private val onItemRemoved: (MapItem) -> Unit, + private val onItemClicked: (MapItem) -> Unit + +) : RecyclerView.Adapter() { + + private var items: List = emptyList() + + + fun submitList(newItems: List) { + items = newItems + notifyDataSetChanged() + } + + //viewHolder 생성 메서드 + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectedViewHolder { + + val binding = ItemSelectedBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return SelectedViewHolder(binding) + } + + override fun onBindViewHolder(holder: SelectedViewHolder, position: Int) { + val currentItem = items[position] + holder.bind(currentItem) + } + + override fun getItemCount(): Int { + return items.size + } + + inner class SelectedViewHolder(private val binding: ItemSelectedBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(item: MapItem) { + binding.apply { + selectedItemName.text = item.place_name //상단에 이름만 표시 + deleteButton.setOnClickListener { onItemRemoved(item) } + + root.setOnClickListener { onItemClicked(item) } // 클릭 시 검색 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/util/Constants.kt b/app/src/main/java/campus/tech/kakao/map/util/Constants.kt new file mode 100644 index 00000000..d49ae67f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/util/Constants.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.util + +import campus.tech.kakao.map.BuildConfig + +object Constants { + const val KAKAO_API_KEY = "KakaoAK ${BuildConfig.KAKAO_REST_API_KEY}" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/util/SQLiteHelpter.kt b/app/src/main/java/campus/tech/kakao/map/util/SQLiteHelpter.kt new file mode 100644 index 00000000..e865a4f6 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/util/SQLiteHelpter.kt @@ -0,0 +1,73 @@ +package campus.tech.kakao.map.util + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.database.Cursor + +class SQLiteHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + + companion object { + private const val DATABASE_NAME = "map.db" + private const val DATABASE_VERSION = 1 + const val TABLE_NAME = "map_table" + const val COL_ID = "id" + const val COL_NAME = "name" + const val COL_ADDRESS = "address" + const val COL_CATEGORY = "category" + } + + + override fun onCreate(db: SQLiteDatabase) { + // 테이블 생성 SQL + val createTableStatement = ("CREATE TABLE $TABLE_NAME (" + + "$COL_ID INTEGER PRIMARY KEY AUTOINCREMENT, " + + "$COL_NAME TEXT, " + + "$COL_ADDRESS TEXT, " + + "$COL_CATEGORY TEXT)") + db.execSQL(createTableStatement) + + // 비어있는지 확인 + if (isTableEmpty(db)) { + insertInitialData(db) + } + } + + // 테이블이 비어있는지 확인 + private fun isTableEmpty(db: SQLiteDatabase): Boolean { + val cursor = db.rawQuery("SELECT COUNT(*) FROM $TABLE_NAME", null) + cursor.moveToFirst() + + val cnt = cursor.getInt(0) + cursor.close() + + return cnt == 0 + } + + // 데이터베이스 업그레이드 시 기존 데이터 유지 + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + } + + // 초기 데이터 삽입 메서드 + // id만 삽입 후 db inspector에서 직접 입력 + private fun insertInitialData(db: SQLiteDatabase) { + for (i in 1..50) { + val values = ContentValues().apply { + put(COL_NAME, "") + put(COL_ADDRESS, "") + put(COL_CATEGORY, "") + } + db.insert(TABLE_NAME, null, values) + } + } + + //항목 검색 + fun searchItems(query: String): Cursor { + val db = this.readableDatabase + return db.rawQuery( //cursor로 반환 + "SELECT * FROM $TABLE_NAME WHERE $COL_NAME LIKE ?", + arrayOf("%$query%") + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt new file mode 100644 index 00000000..a4a379b8 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt @@ -0,0 +1,54 @@ +package campus.tech.kakao.map.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.model.MapItem +import campus.tech.kakao.map.repository.MapRepository +import kotlinx.coroutines.launch + +class MapViewModel(application: Application, private val repository: MapRepository) : AndroidViewModel(application) { + + val searchQuery = MutableLiveData() + + private val _searchResults = MutableLiveData>(emptyList()) + val searchResults: LiveData> = _searchResults + + private val _selectedItems = MutableLiveData>(emptyList()) + val selectedItems: LiveData> get() = _selectedItems + + init { + searchQuery.observeForever { query -> + if (query.isNullOrEmpty()) { + _searchResults.postValue(emptyList()) + } else { + searchItems(query) + } + } + } + + private fun searchItems(query: String) { + viewModelScope.launch { + val results = repository.searchItems(query) + _searchResults.postValue(results) + } + } + + fun selectItem(item: MapItem) { + _selectedItems.value = _selectedItems.value?.plus(item) + } + + fun removeSelectedItem(item: MapItem) { + _selectedItems.value = _selectedItems.value?.minus(item) + } + + fun setSelectedItems(items: List) { + _selectedItems.value = items + } + + fun setSearchResults(results: List) { + _searchResults.value = results + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt new file mode 100644 index 00000000..73ecff73 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModelFactory.kt @@ -0,0 +1,19 @@ +package campus.tech.kakao.map.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.repository.MapRepository + +class MapViewModelFactory( + private val application: Application, + private val repository: MapRepository +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MapViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return MapViewModel(application, repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 00000000..53086a4d --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_location_marker.xml b/app/src/main/res/drawable/ic_location_marker.xml new file mode 100644 index 00000000..9837f7db --- /dev/null +++ b/app/src/main/res/drawable/ic_location_marker.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..2134f6f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/new_marker.png b/app/src/main/res/drawable/new_marker.png new file mode 100644 index 0000000000000000000000000000000000000000..b858ed803a8dfb362005750f70faa4f316b96374 GIT binary patch literal 1943 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|@Cg=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a!@$7g5a1Ky3RLji+xoeW&2w*?XI|FNd~9C$+r0>| zd*f#H!pG)Kp#33hy#too?_69CSZY1y7h?zvkWveLQfYIeib^p3mvBTuVy4u%&TjUIbhow74{;%$A!+2n|| z?gLND({=_&ZFCP?>;C`$f9o%Adte%xQ4-`A45SGFR)eAqw?F-5b6xU8YawIEEk>s7 z_e^P@-CxU`Zqa;HeSKyL|FMrvGNxZM7!&@@W_fQp>x=;7;+^dC4s-WBQ7d))`oLV` z%0*|!NpEHa7}|dL_3qhI6Z?~U^E>9L_sYdjM*iSh|AuLz+8vSO z5i=w10@FPMW0JSK%l^x^H-`c_>?NMQuIw+^1vtf|tS+8^#K6GZ4e*rMH)sTER2#&r^2+A79=r#PtkIAJm8q)=r&=k#^Q*!`q}#L zmtI&C^#1jA{rB(cc2@7Ko*pp8L5AVL{{n{x6Bj&qut2$yvHkDFLyemhwcI&4dAa%1 zDmeN0xp^7gry4cBlMs|V!l9yJ#I$m1=iuZY$UYSR`dLd#M>(+ zy03>HOK_;)b7amfzAVj+$?9|L4)2y=XWP%qTPrPN@TtpqdE#@vnOl<1f8sl=y!efT zP+3XJjPSX=Q$2ahj($|!*Wb5sPfu^%bLOLOnKCE*V97D=@!t?;WSDp0RZE4ZM1xDw zZcm>%*7K*7{F6yj>$D5(Qi(q>ZDr<@X_Klhb}pN%w`|#hqhCbKVm!nYcPsWzNnx{G znjW+0`-Ku#*}|e(m%OAxzT;a^+r-L{soB@f zzZdRHMv~^!{F73bPddgv&n`LT>CNB|7f&cm;hML9f_t0|d+SCXfhcYF zD{WeQsiJ$%Om_Yh*YxNOd&So%uRV23ZGNu&E4}z+)CG%M>pK=0@b)Gx+IY}Spv!7O zfNe`_A=cv@BH>- zE$6ERQdVXY`2ytc2stKN-H+@jpIgS__=x-cZjOszV%uNCq`9;9 zwJTSB`L@)xQ1fw``sS%`t9m64H^ zk(suEft7)Q$=P0D<%gspH$NpatrE8e*9oQ{ff_X6Hk4%MrWThZ<`&@ABh)IQ2h_vh M>FVdQ&MBb@0EF5>HUIzs literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/search_background.xml b/app/src/main/res/drawable/search_background.xml new file mode 100644 index 00000000..a8b409b1 --- /dev/null +++ b/app/src/main/res/drawable/search_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..9db42bba 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,99 @@ - - + xmlns:app="http://schemas.android.com/apk/res-auto" + tools:context=".ui.MainActivity"> + + + + + + + - + + + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" + android:background="@android:color/white"> + + + + + + + + + + + + + + + - + + + 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..d2e4c6ce --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_bottom_sheet.xml b/app/src/main/res/layout/fragment_search_bottom_sheet.xml new file mode 100644 index 00000000..4a7679a7 --- /dev/null +++ b/app/src/main/res/layout/fragment_search_bottom_sheet.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml new file mode 100644 index 00000000..a0c4d626 --- /dev/null +++ b/app/src/main/res/layout/item_search_result.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_selected.xml b/app/src/main/res/layout/item_selected.xml new file mode 100644 index 00000000..67a4a96f --- /dev/null +++ b/app/src/main/res/layout/item_selected.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ 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..c8524cd9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,4 @@ #FF000000 #FFFFFFFF - + \ 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..18ea1798 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ Map + 검색어를 입력해 주세요. + 검색 결과가 없습니다. + 이미 선택된 목록입니다. \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..01ab8ff7 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/example/map/ExampleUnitTest.kt b/app/src/test/java/com/example/map/ExampleUnitTest.kt new file mode 100644 index 00000000..3f4598e7 --- /dev/null +++ b/app/src/test/java/com/example/map/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.map + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d6b55841..ace33df3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,8 @@ plugins { id("com.android.application") version "8.3.1" apply false 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 } allprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") -} +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..55fda39e --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,22 @@ +[versions] +agp = "8.3.2" +kotlin = "1.9.0" +coreKtx = "1.13.1" +junit = "4.13.2" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +appcompat = "1.7.0" +material = "1.12.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/settings.gradle.kts b/settings.gradle.kts index 48bba0fc..fe9e1af2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,14 +1,10 @@ pluginManagement { repositories { - google { - content { - includeGroupByRegex("com\\.android.*") - includeGroupByRegex("com\\.google.*") - includeGroupByRegex("androidx.*") - } - } + google() mavenCentral() gradlePluginPortal() + maven { url = uri("https://devrepo.kakao.com/nexus/content/groups/public/") } + maven { url = uri("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/")} } } dependencyResolutionManagement { @@ -16,9 +12,11 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") + maven { url = uri("https://devrepo.kakao.com/nexus/content/groups/public/") } + maven { url = uri("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") } } } -rootProject.name = "android-map-refactoring" +rootProject.name = "android-map-search" include(":app") +include(":app") \ No newline at end of file