diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c1a15f2..e2ccd648 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -19,6 +21,10 @@ 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")) + } buildTypes { @@ -39,10 +45,15 @@ android { } buildFeatures { + viewBinding = true dataBinding = true buildConfig = true } + testOptions { + animationsDisabled = true + } } +fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key) dependencies { @@ -58,6 +69,8 @@ dependencies { 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") + implementation("androidx.test.espresso:espresso-contrib:3.6.1") + implementation("androidx.test.ext:junit-ktx:1.2.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") diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt new file mode 100644 index 00000000..d66805c4 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt @@ -0,0 +1,95 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.content.Intent +import androidx.test.core.app.ApplicationProvider +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withHint +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.presentation.MapActivity +import campus.tech.kakao.map.presentation.PlaceActivity +import org.hamcrest.CoreMatchers.not +import org.junit.Rule +import org.junit.Test + +class MapActivityTest { + @get:Rule + val activityRule = ActivityScenarioRule(MapActivity::class.java) + + + @Test + fun 검색필드가_표시되는지_확인_테스트() { + onView(withId(R.id.searchBox)).check(matches(isDisplayed())) + } + + @Test + fun 검색필드의_힌트텍스트_확인_테스트() { + onView(withId(R.id.searchTextView)).check(matches(withHint("검색어를 입력해 주세요"))) + } + + @Test + fun 맵뷰가_표시되는지_확인_테스트() { + onView(withId(R.id.mapView)).check(matches(isDisplayed())) + } + + @Test + fun 검색필드를_누르면_액티비티_전환되는지_확인_테스트() { + Intents.init() + onView(withId(R.id.searchBox)).perform(click()) + Intents.intended(hasComponent(PlaceActivity::class.java.name)) + Intents.release() + } + + + @Test + fun 인텐트가_있을때_바텀시트가_표시되는지_확인() { + val place = PlaceVO("가짜 장소", "가짜 주소", "카페", 37.5665, 126.9780) + val intent = + Intent(ApplicationProvider.getApplicationContext(), MapActivity::class.java).apply { + putExtra("place", place) + } + + launchActivity(intent) + onView(withId(R.id.bottomSheet)).check(matches(isDisplayed())) + } + + @Test + fun 바텀시트에_표시된_텍스트가_인텐트로_보낸것과_일치하는지_확인() { + val place = PlaceVO("가짜 장소", "가짜 주소", "카페", 37.5665, 126.9780) + val intent = + Intent(ApplicationProvider.getApplicationContext(), MapActivity::class.java).apply { + putExtra("place", place) + } + launchActivity(intent) + + onView(withId(R.id.nameTextaView)).check(matches(withText("가짜 장소"))) + onView(withId(R.id.addressTextView)).check(matches(withText("가짜 주소"))) + } + + @Test + fun 인텐트와_마지막장소가_없다면_디폴트맵_표시하는지_확인_테스트() { + + // 마지막 장소 초기화 + val context = ApplicationProvider.getApplicationContext() + val sharedPreferences = context.getSharedPreferences("kakao_map", Context.MODE_PRIVATE) + sharedPreferences.edit().clear().apply() + + launchActivity() + + onView(withId(R.id.mapView)).check(matches(isDisplayed())) + onView(withId(R.id.bottomSheet)).check(matches(not(isDisplayed()))) + + + } + + +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/PlaceActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/PlaceActivityTest.kt new file mode 100644 index 00000000..540a989d --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/PlaceActivityTest.kt @@ -0,0 +1,124 @@ +package campus.tech.kakao.map + + +import android.content.pm.ActivityInfo +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.espresso.contrib.RecyclerViewActions +import campus.tech.kakao.map.presentation.MapActivity +import campus.tech.kakao.map.presentation.PlaceActivity +import campus.tech.kakao.map.presentation.PlaceAdapter +import campus.tech.kakao.map.presentation.SearchHistoryAdapter +import org.hamcrest.CoreMatchers.not +import org.junit.Rule +import org.junit.Test + +class PlaceActivityTest { + @get:Rule + val activityRule = ActivityScenarioRule(PlaceActivity::class.java) + + @Test + fun 검색박스에_텍스트_입력_시_검색_결과가_표시되는지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("N")) + Thread.sleep(3000) + onView(withId(R.id.placeRecyclerView)).check(matches(isDisplayed())) + } + + @Test + fun 검색_아이템_클릭_시_히스토리에_잘_저장되는지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("N")) + Thread.sleep(2000) + onView(withId(R.id.placeRecyclerView)).perform( + RecyclerViewActions.actionOnItemAtPosition(0, click()) + ) + Espresso.pressBack() + onView(withId(R.id.historyRecyclerView)).check(matches(hasDescendant(withText("N")))) + } + + @Test + fun 검색_결과_클릭_시_지도_액티비티로_넘어가는지_테스트() { + Intents.init() + onView(withId(R.id.searchEditText)).perform(typeText("N")) + Thread.sleep(1000) + onView(withId(R.id.placeRecyclerView)).perform( + RecyclerViewActions.actionOnItemAtPosition(0, click()) + ) + Intents.intended(hasComponent(MapActivity::class.java.name)) + + Intents.release() + } + + @Test + fun 검색_히스토리_항목_클릭_시_검색_결과_표시되는지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("Z")) + Thread.sleep(3000) + onView(withId(R.id.placeRecyclerView)).perform( + RecyclerViewActions.actionOnItemAtPosition(0, click()) + ) + Espresso.pressBack() + onView(withId(R.id.historyRecyclerView)).perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, + click() + ) + ) + Thread.sleep(3000) + onView(withId(R.id.placeRecyclerView)).check(matches(isDisplayed())) + } + + @Test + fun 검색_히스토리에서_삭제_시_히스토리에서_제거되는지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("N")) + Thread.sleep(1000) + onView(withId(R.id.historyRecyclerView)).perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, + click() + ) + ) + Thread.sleep(1000) + onView(withId(R.id.historyRecyclerView)).check(matches(not(hasDescendant(withText("N"))))) + } + + @Test + fun 검색박스에서_텍스트_지우면_검색_결과가_사라지고_빈_메시지_표시되는지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("N")) + onView(withId(R.id.cancelButton)).perform(click()) + Thread.sleep(2000) + onView(withId(R.id.emptyMessage)).check(matches(isDisplayed())) + onView(withId(R.id.placeRecyclerView)).check(matches(not(isDisplayed()))) + } + + @Test + fun 검색박스에서_x_버튼누르면_텍스트_지워지는지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("N")) + onView(withId(R.id.cancelButton)).perform(click()) + onView(withId(R.id.searchEditText)).check(matches(withText(""))) + } + + @Test + fun 기기_회전_후에_검색_히스토리가_유지되는지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("N")) + activityRule.scenario.onActivity { + it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } + onView(withId(R.id.historyRecyclerView)).check(matches(hasDescendant(withText("N")))) + } + + @Test + fun 검색_결과가_스크롤_가능한지_테스트() { + onView(withId(R.id.searchEditText)).perform(typeText("N")) + Thread.sleep(3000) + onView(withId(R.id.placeRecyclerView)).perform( + RecyclerViewActions.scrollToPosition(10) + ) + onView(withId(R.id.placeRecyclerView)).check(matches(isDisplayed())) + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..8c86c630 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,15 @@ android:supportsRtl="true" android:theme="@style/Theme.Map" tools:targetApi="31"> + + + 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..4852786c 100644 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt @@ -1,11 +1,18 @@ package campus.tech.kakao.map +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.presentation.MapActivity +import campus.tech.kakao.map.presentation.PlaceActivity + class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + intent = Intent(this, MapActivity::class.java) + startActivity(intent) + finish() } } diff --git a/app/src/main/java/campus/tech/kakao/map/data/PreferenceHelper.kt b/app/src/main/java/campus/tech/kakao/map/data/PreferenceHelper.kt new file mode 100644 index 00000000..0332d9fd --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/PreferenceHelper.kt @@ -0,0 +1,61 @@ +package campus.tech.kakao.map.data + +import android.content.Context +import android.content.SharedPreferences +import campus.tech.kakao.map.domain.model.PlaceVO +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + + +object PreferenceHelper { + private const val PREF_NAME = "kakao_map" + private const val PREF_KEY_SEARCH_QUERY = "search_query" + private const val PREF_KEY_LAST_PLACE = "last_place" + + fun defaultPrefs(context: Context): SharedPreferences = + context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + + fun getSearchHistory(context: Context): List { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val jsonSearchHistory = prefs.getString(PREF_KEY_SEARCH_QUERY, null) + + return if (jsonSearchHistory != null) { + val type = object : TypeToken>() {}.type + Gson().fromJson(jsonSearchHistory, type) + } else { + emptyList() + } + } + + fun removeSearchQuery(context: Context, query: String) { + val searchHistory = getSearchHistory(context).toMutableList() + searchHistory.remove(query) + val jsonSearchHistory = Gson().toJson(searchHistory) + defaultPrefs(context).edit().putString(PREF_KEY_SEARCH_QUERY, jsonSearchHistory).apply() + } + + fun saveSearchQuery(context: Context, query: String) { + val searchHistory = getSearchHistory(context).toMutableList() + searchHistory.remove(query) + searchHistory.add(0, query) + val jsonSearchHistory = Gson().toJson(searchHistory) + + defaultPrefs(context).edit().putString(PREF_KEY_SEARCH_QUERY, jsonSearchHistory).apply() + } + + fun saveLastPlace(context: Context, place: PlaceVO) { + val jsonLastPlace = Gson().toJson(place) + defaultPrefs(context).edit().putString(PREF_KEY_LAST_PLACE, jsonLastPlace).apply() + } + + fun getLastPlace(context: Context): PlaceVO? { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val jsonLastPlace = prefs.getString(PREF_KEY_LAST_PLACE, null) + + return if (jsonLastPlace != null) { + Gson().fromJson(jsonLastPlace, PlaceVO::class.java) + } else { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/model/DocumentEntity.kt b/app/src/main/java/campus/tech/kakao/map/data/model/DocumentEntity.kt new file mode 100644 index 00000000..49b2281a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/model/DocumentEntity.kt @@ -0,0 +1,25 @@ +package campus.tech.kakao.map.data.model + +import com.google.gson.annotations.SerializedName + +data class DocumentEntity( + val id: String, + @SerializedName("place_name") + val placeName: String, + @SerializedName("category_name") + val categoryName: String, + @SerializedName("category_group_code") + val categoryGroupCode: String?, + @SerializedName("category_group_name") + val categoryGroupName: String, + val phone: String?, + @SerializedName("address_name") + val addressName: String, + @SerializedName("road_address_name") + val roadAddressName: String?, + val x: String, + val y: String, + @SerializedName("place_url") + val placeUrl: String, + val distance: String? +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/model/MetaEntity.kt b/app/src/main/java/campus/tech/kakao/map/data/model/MetaEntity.kt new file mode 100644 index 00000000..f9a955a0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/model/MetaEntity.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map.data.model + +import com.google.gson.annotations.SerializedName + +data class MetaEntity( + @SerializedName("total_count") + val totalCount: Int, + @SerializedName("pageable_count") + val pageableCount: Int, + @SerializedName("is_end") + val isEnd: Boolean, +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/model/SearchResponse.kt b/app/src/main/java/campus/tech/kakao/map/data/model/SearchResponse.kt new file mode 100644 index 00000000..ba47b538 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/model/SearchResponse.kt @@ -0,0 +1,6 @@ +package campus.tech.kakao.map.data.model + +data class SearchResponse( + val meta: MetaEntity, + val documents: List +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/network/HttpService.kt b/app/src/main/java/campus/tech/kakao/map/data/network/HttpService.kt new file mode 100644 index 00000000..58bee3fd --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/network/HttpService.kt @@ -0,0 +1,37 @@ +package campus.tech.kakao.map.data.network + +import android.util.Log +import campus.tech.kakao.map.data.model.SearchResponse +import campus.tech.kakao.map.utils.ApiKeyProvider +import com.google.gson.Gson +import java.net.HttpURLConnection +import java.net.URL + +object HttpService { + private const val BASE_URL = "https://dapi.kakao.com/" + + fun searchKeyword(query: String, callback: (SearchResponse?) -> Unit) { + Thread { + val url = "${BASE_URL}v2/local/search/keyword.json?query=$query" + val connection = URL(url).openConnection() as HttpURLConnection + try { + connection.requestMethod = "GET" + connection.setRequestProperty("Authorization", ApiKeyProvider.KAKAO_REST_API_KEY) + val responseCode = connection.responseCode + if (responseCode == HttpURLConnection.HTTP_OK) { + val response = connection.inputStream.bufferedReader().readText() + callback(Gson().fromJson(response, SearchResponse::class.java)) + } else { + Log.d( + "testt", + "Response failed: ${connection.errorStream.bufferedReader().readText()}" + ) + callback(null) + } + } finally { + connection.disconnect() + } + }.start() + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/network/RetrofitObject.kt b/app/src/main/java/campus/tech/kakao/map/data/network/RetrofitObject.kt new file mode 100644 index 00000000..60d06bec --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/network/RetrofitObject.kt @@ -0,0 +1,19 @@ +package campus.tech.kakao.map.data.network + +import campus.tech.kakao.map.data.model.SearchResponse +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitObject { + private const val BASE_URL = "https://dapi.kakao.com/" + + val retrofitService = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(RetrofitService::class.java) + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/network/RetrofitService.kt b/app/src/main/java/campus/tech/kakao/map/data/network/RetrofitService.kt new file mode 100644 index 00000000..5958b68a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/network/RetrofitService.kt @@ -0,0 +1,17 @@ +package campus.tech.kakao.map.data.network + +import campus.tech.kakao.map.data.model.SearchResponse +import campus.tech.kakao.map.utils.ApiKeyProvider +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface RetrofitService { + @GET("v2/local/search/keyword.json") + fun searchKeyword( + @Header("Authorization") apiKey: String = ApiKeyProvider.KAKAO_REST_API_KEY, + @Query("query") query: String, + ): Call + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/repository/PlaceRepositoryImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/repository/PlaceRepositoryImpl.kt new file mode 100644 index 00000000..c6feb5f3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/repository/PlaceRepositoryImpl.kt @@ -0,0 +1,61 @@ +package campus.tech.kakao.map.data.repository + +import android.content.Context +import android.util.Log +import campus.tech.kakao.map.data.PreferenceHelper +import campus.tech.kakao.map.data.model.SearchResponse +import campus.tech.kakao.map.data.network.HttpService +import campus.tech.kakao.map.data.network.RetrofitObject +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.domain.repository.PlaceRepository +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class PlaceRepositoryImpl(private val context: Context) : PlaceRepository { +// private val retrofitService = RetrofitObject.retrofitService + override fun searchPlaces(query: String, callback: (List?) -> Unit) { + HttpService.searchKeyword(query = query){ response -> +// override fun onResponse( +// call: Call, +// response: Response +// ) { + if (response != null) { + val places = response?.documents?.map { + PlaceVO( + placeName = it.placeName, + addressName = it.addressName, + categoryName = it.categoryGroupName, + latitude = it.y.toDouble(), + longitude = it.x.toDouble() + ) + } + callback(places) + } else { + Log.d("testt", "Response failed: ${response.toString()}") + callback(null) + } + } + } + + + override fun saveSearchQuery(place: PlaceVO) { + PreferenceHelper.saveSearchQuery(context, place.placeName) + } + + override fun getSearchHistory(): List { + return PreferenceHelper.getSearchHistory(context) + } + + override fun removeSearchQuery(query: String) { + PreferenceHelper.removeSearchQuery(context, query) + } + + override fun saveLastPlace(place: PlaceVO) { + PreferenceHelper.saveLastPlace(context, place) + } + + override fun getLastPlace(): PlaceVO? { + return PreferenceHelper.getLastPlace(context) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/usecase/GetLastPlaceUseCaseImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/usecase/GetLastPlaceUseCaseImpl.kt new file mode 100644 index 00000000..f7b0ad1b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/usecase/GetLastPlaceUseCaseImpl.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.data.usecase + +import android.util.Log +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.domain.repository.PlaceRepository +import campus.tech.kakao.map.domain.usecase.GetLastPlaceUseCase + +class GetLastPlaceUseCaseImpl (private val placeRepository: PlaceRepository): GetLastPlaceUseCase { + override fun invoke(): PlaceVO? { + return placeRepository.getLastPlace() + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/usecase/GetSearchHistoryUseCaseImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/usecase/GetSearchHistoryUseCaseImpl.kt new file mode 100644 index 00000000..326ab8a0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/usecase/GetSearchHistoryUseCaseImpl.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map.data.usecase + +import campus.tech.kakao.map.domain.repository.PlaceRepository +import campus.tech.kakao.map.domain.usecase.GetSearchHistoryUseCase +import campus.tech.kakao.map.domain.usecase.GetSearchPlacesUseCase + +class GetSearchHistoryUseCaseImpl(private val placeRepository: PlaceRepository) : + GetSearchHistoryUseCase { + override fun invoke(): List { + return placeRepository.getSearchHistory() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/usecase/GetSearchPlacesUseCaseImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/usecase/GetSearchPlacesUseCaseImpl.kt new file mode 100644 index 00000000..a6eb943f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/usecase/GetSearchPlacesUseCaseImpl.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.data.usecase + +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.domain.repository.PlaceRepository +import campus.tech.kakao.map.domain.usecase.GetSearchPlacesUseCase + +class GetSearchPlacesUseCaseImpl(private val placeRepository: PlaceRepository) : + GetSearchPlacesUseCase { + override fun invoke(query: String, callback: (List?) -> Unit) { + placeRepository.searchPlaces(query, callback) + } + +} diff --git a/app/src/main/java/campus/tech/kakao/map/data/usecase/RemoveSearchQueryUseCaseImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/usecase/RemoveSearchQueryUseCaseImpl.kt new file mode 100644 index 00000000..d607b3a6 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/usecase/RemoveSearchQueryUseCaseImpl.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.data.usecase + +import campus.tech.kakao.map.domain.repository.PlaceRepository +import campus.tech.kakao.map.domain.usecase.GetSearchPlacesUseCase +import campus.tech.kakao.map.domain.usecase.RemoveSearchQueryUseCase + +class RemoveSearchQueryUseCaseImpl(private val placeRepository: PlaceRepository) : + RemoveSearchQueryUseCase { + override fun invoke(query: String) { + placeRepository.removeSearchQuery(query) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/usecase/SaveLastPlaceUseCaseImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/usecase/SaveLastPlaceUseCaseImpl.kt new file mode 100644 index 00000000..4303af4b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/usecase/SaveLastPlaceUseCaseImpl.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.data.usecase + +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.domain.repository.PlaceRepository +import campus.tech.kakao.map.domain.usecase.GetLastPlaceUseCase +import campus.tech.kakao.map.domain.usecase.SaveLastPlaceUseCase + +class SaveLastPlaceUseCaseImpl(private val placeRepository: PlaceRepository): SaveLastPlaceUseCase { + override fun invoke(place: PlaceVO) { + placeRepository.saveLastPlace(place) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/usecase/SaveSearchQueryUseCaseImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/usecase/SaveSearchQueryUseCaseImpl.kt new file mode 100644 index 00000000..b3da6e26 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/usecase/SaveSearchQueryUseCaseImpl.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.data.usecase + +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.domain.repository.PlaceRepository +import campus.tech.kakao.map.domain.usecase.SaveSearchQueryUseCase + +class SaveSearchQueryUseCaseImpl(private val placeRepository: PlaceRepository) : + SaveSearchQueryUseCase { + override fun invoke(place: PlaceVO) { + placeRepository.saveSearchQuery(place) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/domain/model/PlaceVO.kt b/app/src/main/java/campus/tech/kakao/map/domain/model/PlaceVO.kt new file mode 100644 index 00000000..869d75d6 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/model/PlaceVO.kt @@ -0,0 +1,11 @@ +package campus.tech.kakao.map.domain.model + +import java.io.Serializable + +data class PlaceVO( + val placeName: String, + val addressName: String, + val categoryName: String, + val latitude: Double, + val longitude: Double, +) : Serializable diff --git a/app/src/main/java/campus/tech/kakao/map/domain/repository/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/domain/repository/PlaceRepository.kt new file mode 100644 index 00000000..b12a0cb0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/repository/PlaceRepository.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map.domain.repository + +import campus.tech.kakao.map.domain.model.PlaceVO + +interface PlaceRepository { + fun searchPlaces(query: String, callback: (List?) -> Unit) + fun saveSearchQuery(place: PlaceVO) + fun getSearchHistory(): List + fun removeSearchQuery(query: String) + fun saveLastPlace(place: PlaceVO) + fun getLastPlace(): PlaceVO? +} diff --git a/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetLastPlaceUseCase.kt b/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetLastPlaceUseCase.kt new file mode 100644 index 00000000..d4e17670 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetLastPlaceUseCase.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.domain.usecase + +import campus.tech.kakao.map.domain.model.PlaceVO + +interface GetLastPlaceUseCase { + operator fun invoke(): PlaceVO? +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetSearchHistoryUseCase.kt b/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetSearchHistoryUseCase.kt new file mode 100644 index 00000000..c01173c2 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetSearchHistoryUseCase.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.domain.usecase + +interface GetSearchHistoryUseCase { + operator fun invoke(): List +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetSearchPlacesUseCase.kt b/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetSearchPlacesUseCase.kt new file mode 100644 index 00000000..8c0f499c --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/usecase/GetSearchPlacesUseCase.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.domain.usecase + +import campus.tech.kakao.map.domain.model.PlaceVO + +interface GetSearchPlacesUseCase { + operator fun invoke(query: String, callback: (List?) -> Unit) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/domain/usecase/RemoveSearchQueryUseCase.kt b/app/src/main/java/campus/tech/kakao/map/domain/usecase/RemoveSearchQueryUseCase.kt new file mode 100644 index 00000000..5b56be53 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/usecase/RemoveSearchQueryUseCase.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.domain.usecase + +interface RemoveSearchQueryUseCase { + operator fun invoke(query: String) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/domain/usecase/SaveLastPlaceUseCase.kt b/app/src/main/java/campus/tech/kakao/map/domain/usecase/SaveLastPlaceUseCase.kt new file mode 100644 index 00000000..3083604d --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/usecase/SaveLastPlaceUseCase.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.domain.usecase + +import campus.tech.kakao.map.domain.model.PlaceVO + +interface SaveLastPlaceUseCase { + operator fun invoke(place: PlaceVO) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/domain/usecase/SaveSearchQueryUseCase.kt b/app/src/main/java/campus/tech/kakao/map/domain/usecase/SaveSearchQueryUseCase.kt new file mode 100644 index 00000000..5991356c --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/usecase/SaveSearchQueryUseCase.kt @@ -0,0 +1,7 @@ +package campus.tech.kakao.map.domain.usecase + +import campus.tech.kakao.map.domain.model.PlaceVO + +interface SaveSearchQueryUseCase { + operator fun invoke(place: PlaceVO) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/ErrorActivity.kt b/app/src/main/java/campus/tech/kakao/map/presentation/ErrorActivity.kt new file mode 100644 index 00000000..4df8dc2f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/ErrorActivity.kt @@ -0,0 +1,18 @@ +package campus.tech.kakao.map.presentation + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import campus.tech.kakao.map.databinding.ActivityErrorBinding + +class ErrorActivity : AppCompatActivity() { + private lateinit var binding: ActivityErrorBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityErrorBinding.inflate(layoutInflater) + setContentView(binding.root) + + val errorMessage = intent.getStringExtra("error") + binding.errorContent.text = errorMessage ?: "알 수 없는 에러가 발생했습니다." + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/presentation/MapActivity.kt new file mode 100644 index 00000000..483fc52e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/MapActivity.kt @@ -0,0 +1,175 @@ +package campus.tech.kakao.map.presentation + +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.cardview.widget.CardView +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.R +import campus.tech.kakao.map.data.repository.PlaceRepositoryImpl +import campus.tech.kakao.map.data.usecase.GetLastPlaceUseCaseImpl +import campus.tech.kakao.map.data.usecase.SaveLastPlaceUseCaseImpl +import campus.tech.kakao.map.databinding.ActivityMapBinding +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.utils.ApiKeyProvider +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.CameraAnimation +import com.kakao.vectormap.camera.CameraPosition +import com.kakao.vectormap.camera.CameraUpdateFactory +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles + + +class MapActivity : AppCompatActivity() { + private lateinit var binding: ActivityMapBinding + private lateinit var mapView: MapView + private lateinit var searchBox: CardView + private lateinit var kakaoMap: KakaoMap + private lateinit var mapViewModel: MapViewModel + private lateinit var bottomSheetBehavior: BottomSheetBehavior + + override fun onCreate(savedInstanceState: Bundle?) { + KakaoMapSdk.init(this, ApiKeyProvider.KAKAO_API_KEY) + super.onCreate(savedInstanceState) + binding = ActivityMapBinding.inflate(layoutInflater) + setContentView(binding.root) + + bindingViews() + initializeViewModel() + binding.viewModel = mapViewModel + binding.lifecycleOwner = this + + handleIntentData() + setUpMap() + setUpSearchBox() + } + + private fun bindingViews() { + mapView = binding.mapView + searchBox = binding.searchBox + bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomSheet) + } + + private fun setUpSearchBox() { + searchBox.setOnClickListener { + startActivity(Intent(this, PlaceActivity::class.java)) + } + } + + private fun initializeViewModel() { + val placeRepository = PlaceRepositoryImpl(context = this) + val factory = MapViewModelFactory( + SaveLastPlaceUseCaseImpl(placeRepository), + GetLastPlaceUseCaseImpl(placeRepository) + ) + mapViewModel = ViewModelProvider(this, factory).get(MapViewModel::class.java) + } + + private fun setUpMap() { + if (mapViewModel.lastPlace.value != null) { + startMap(mapViewModel.lastPlace.value as PlaceVO) + } else { + startDefaultMap() + } + } + + private fun handleIntentData() { + val placeFromIntent = intent.getSerializableExtra("place") + if (placeFromIntent != null) { + mapViewModel.setLastPlace(placeFromIntent) + } + } + + private fun startMap(place: PlaceVO) { + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + Log.d("testt", "지도 종료됨") + } + + override fun onMapError(error: Exception) { + handleError(error) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + this@MapActivity.kakaoMap = kakaoMap + setCameraPosition(place.latitude, place.longitude) + addMarker(place) + displayBottomSheet(place) + mapViewModel.saveLastPlace(place) + } + }) + } + + private fun startDefaultMap() { + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + //no-op + } + + override fun onMapError(error: Exception) { + handleError(error) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(kakaoMap: KakaoMap) { + this@MapActivity.kakaoMap = kakaoMap + setCameraPosition(37.5665, 126.9780) + } + }) + } + + private fun setCameraPosition(latitude: Double, longitude: Double) { + val cameraPositionBuilder = CameraPosition.Builder().apply { + setPosition(LatLng.from(latitude, longitude)) + setZoomLevel(21) + } + + kakaoMap.moveCamera( + CameraUpdateFactory.newCameraPosition( + CameraPosition.from( + cameraPositionBuilder + ) + ), CameraAnimation.from(500, true, true) + ) + } + + private fun addMarker(place: PlaceVO) { + var styles = LabelStyles.from( + "marker", + LabelStyle.from(R.drawable.marker).setZoomLevel(8) + .setTextStyles(32, Color.BLACK, 1, Color.GRAY).setZoomLevel(15) + ) + + styles = kakaoMap.labelManager!!.addLabelStyles(styles!!) + + val label = kakaoMap.labelManager!!.layer!!.addLabel( + LabelOptions.from(LatLng.from(place.latitude, place.longitude)) + .setTexts(place.placeName) + .setStyles(styles) + ) + } + + private fun displayBottomSheet(place: PlaceVO) { + binding.nameTextaView.text = place.placeName + binding.addressTextView.text = place.addressName + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + private fun handleError(error: Exception) { + Log.d("testt", error.message ?: "Unknown error") + runOnUiThread { + val intent = Intent(this@MapActivity, ErrorActivity::class.java) + intent.putExtra("error", error.message) + startActivity(intent) + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/MapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/presentation/MapViewModel.kt new file mode 100644 index 00000000..aa374a45 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/MapViewModel.kt @@ -0,0 +1,38 @@ +package campus.tech.kakao.map.presentation + +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import campus.tech.kakao.map.domain.model.PlaceVO +import campus.tech.kakao.map.domain.usecase.GetLastPlaceUseCase +import campus.tech.kakao.map.domain.usecase.SaveLastPlaceUseCase +import java.io.Serializable + + +class MapViewModel( + private val saveLastPlaceUseCase: SaveLastPlaceUseCase, + private val getLastPlaceUseCase: GetLastPlaceUseCase +) : ViewModel() { + + private val _lastPlace = MutableLiveData() + val lastPlace: MutableLiveData get() = _lastPlace + + + init { + getLastPlace() + } + + + fun saveLastPlace(place: PlaceVO) { + saveLastPlaceUseCase(place) + _lastPlace.postValue(place) + } + + private fun getLastPlace() { + _lastPlace.value = getLastPlaceUseCase() + } + + fun setLastPlace(place: Serializable) { + _lastPlace.value = place as PlaceVO + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/MapViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/presentation/MapViewModelFactory.kt new file mode 100644 index 00000000..67b85b82 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/MapViewModelFactory.kt @@ -0,0 +1,18 @@ +package campus.tech.kakao.map.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.domain.usecase.GetLastPlaceUseCase +import campus.tech.kakao.map.domain.usecase.SaveLastPlaceUseCase + +class MapViewModelFactory( + private val saveLastPlaceUseCase: SaveLastPlaceUseCase, + private val getLastPlaceUseCase: GetLastPlaceUseCase +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MapViewModel::class.java)) { + return MapViewModel(saveLastPlaceUseCase, getLastPlaceUseCase) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/PlaceActivity.kt b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceActivity.kt new file mode 100644 index 00000000..0dd5904e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceActivity.kt @@ -0,0 +1,151 @@ +package campus.tech.kakao.map.presentation + +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.data.repository.PlaceRepositoryImpl +import campus.tech.kakao.map.data.usecase.*; +import campus.tech.kakao.map.databinding.ActivityPlaceBinding +import campus.tech.kakao.map.domain.model.PlaceVO +import com.kakao.vectormap.LatLng +import com.kakao.vectormap.camera.CameraPosition + +class PlaceActivity : AppCompatActivity() { + private lateinit var placeViewModel: PlaceViewModel + private lateinit var placeAdapter: PlaceAdapter + private lateinit var historyAdapter: SearchHistoryAdapter + + private lateinit var binding: ActivityPlaceBinding + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPlaceBinding.inflate(layoutInflater) + setContentView(binding.root) + + initializeViewModel() + initializeAdapters() + initializeRecyclerView() + + setUpSearchEditText() + setUpRemoveButton() + } + + private fun showEmptyMessage() { + binding.emptyMessage.visibility = TextView.VISIBLE + binding.placeRecyclerView.visibility = RecyclerView.GONE + } + + private fun clearSearchEditText() { + binding.searchEditText.text.clear() + } + + private fun showRecyclerView(places: List) { + binding.emptyMessage.visibility = TextView.GONE + binding.placeRecyclerView.visibility = RecyclerView.VISIBLE + placeAdapter.updateData(places) + } + + + private fun initializeViewModel() { + val placeRepository = PlaceRepositoryImpl(context = this) + + placeViewModel = ViewModelProvider( + this, + PlaceViewModelFactory( + getSearchPlacesUseCase = GetSearchPlacesUseCaseImpl(placeRepository), + saveSearchQueryUseCase = SaveSearchQueryUseCaseImpl(placeRepository), + getSearchHistoryUseCase = GetSearchHistoryUseCaseImpl(placeRepository), + removeSearchQueryUseCase = RemoveSearchQueryUseCaseImpl(placeRepository), + ) + )[PlaceViewModel::class.java] + + placeViewModel.places.observe(this) { places -> + updateUI(places) + } + + placeViewModel.searchHistory.observe(this) { history -> + historyAdapter.updateData(history) + } + placeViewModel.loadSearchHistory() + } + + private fun updateUI(places: List) { + if (places.isEmpty()) { + showEmptyMessage() + } else { + showRecyclerView(places) + } + } + + private fun initializeRecyclerView() { + binding.placeRecyclerView.layoutManager = LinearLayoutManager(this) + binding.placeRecyclerView.adapter = placeAdapter + + binding.historyRecyclerView.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + binding.historyRecyclerView.adapter = historyAdapter + } + + private fun initializeAdapters() { + placeAdapter = PlaceAdapter { place -> + placeViewModel.saveSearchQuery(place) + sendPositiontoMap(place) + } + + historyAdapter = SearchHistoryAdapter( + historyList = mutableListOf(), + onDeleteClick = { query -> + placeViewModel.removeSearchQuery(query) + }, + onItemClick = { query -> + binding.searchEditText.setText(query) + placeViewModel.searchPlaces(query) + } + ) + } + private fun sendPositiontoMap(place: PlaceVO) { + val latlng = placeViewModel.getPlaceLocation(place) + val intent = Intent(this, MapActivity::class.java).apply { + putExtra("place", place) + } + startActivity(intent) + } + + private fun setUpSearchEditText() { + binding.searchEditText.addTextChangedListener( + object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + //no-op} + } + + override fun onTextChanged( + p0: CharSequence?, p1: Int, p2: Int, p3: Int, + ) { + if (p0.isNullOrBlank()) { + showEmptyMessage() + return + } + placeViewModel.searchPlaces(query = p0.toString()) + } + + override fun afterTextChanged(p0: Editable?) { + //no-op + } + }, + ) + } + + private fun setUpRemoveButton() { + binding.cancelButton.setOnClickListener { + clearSearchEditText() + showEmptyMessage() + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/PlaceAdapter.kt b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceAdapter.kt new file mode 100644 index 00000000..9f112d80 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceAdapter.kt @@ -0,0 +1,48 @@ +package campus.tech.kakao.map.presentation + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.databinding.PlaceItemBinding +import campus.tech.kakao.map.domain.model.PlaceVO + +class PlaceAdapter( + private val onItemClick: (PlaceVO) -> Unit, +) : RecyclerView.Adapter() { + private var places: List = emptyList() + + class PlaceViewHolder(private val binding: PlaceItemBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(place: PlaceVO, onItemClick: (PlaceVO) -> Unit) { + binding.place = place + binding.clickListener = View.OnClickListener { + onItemClick(place) + } + binding.executePendingBindings() + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): PlaceViewHolder { + val binding = PlaceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PlaceViewHolder(binding) + } + + override fun getItemCount(): Int { + return places.size + } + + override fun onBindViewHolder( + holder: PlaceViewHolder, + position: Int, + ) { + holder.bind(places[position], onItemClick) + } + + fun updateData(newPlaces: List) { + places = newPlaces + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/PlaceViewModel.kt b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceViewModel.kt new file mode 100644 index 00000000..85621ee0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceViewModel.kt @@ -0,0 +1,48 @@ +package campus.tech.kakao.map.presentation + +import campus.tech.kakao.map.domain.usecase.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import campus.tech.kakao.map.domain.model.PlaceVO +import com.kakao.vectormap.LatLng + + +class PlaceViewModel( + private val getSearchPlacesUseCase: GetSearchPlacesUseCase, + private val saveSearchQueryUseCase: SaveSearchQueryUseCase, + private val getSearchHistoryUseCase: GetSearchHistoryUseCase, + private val removeSearchQueryUseCase: RemoveSearchQueryUseCase, +) : ViewModel() { + private val _places = MutableLiveData>() + val places: LiveData> get() = _places + private val _searchHistory = MutableLiveData>() + val searchHistory: LiveData> get() = _searchHistory + + fun searchPlaces(query: String) { + getSearchPlacesUseCase(query) { + _places.postValue(it) + } + } + + fun saveSearchQuery(place: PlaceVO) { + saveSearchQueryUseCase(place) + loadSearchHistory() + } + + fun loadSearchHistory() { + val history = getSearchHistoryUseCase() + _searchHistory.postValue(history.toList()) + } + + fun removeSearchQuery(query: String) { + removeSearchQueryUseCase(query) + loadSearchHistory() + } + + fun getPlaceLocation(place: PlaceVO): LatLng { + // PlaceVO에서 위치 정보를 가져오는 로직 + return LatLng.from(place.latitude, place.longitude) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/PlaceViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceViewModelFactory.kt new file mode 100644 index 00000000..e24fb510 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/PlaceViewModelFactory.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.domain.usecase.*; + +class PlaceViewModelFactory(private val getSearchPlacesUseCase: GetSearchPlacesUseCase, + private val saveSearchQueryUseCase: SaveSearchQueryUseCase, + private val getSearchHistoryUseCase: GetSearchHistoryUseCase, + private val removeSearchQueryUseCase: RemoveSearchQueryUseCase +) : + ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(PlaceViewModel::class.java)) { + return PlaceViewModel( + getSearchPlacesUseCase, + saveSearchQueryUseCase, + getSearchHistoryUseCase, + removeSearchQueryUseCase + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/SearchHistoryAdapter.kt b/app/src/main/java/campus/tech/kakao/map/presentation/SearchHistoryAdapter.kt new file mode 100644 index 00000000..99774bcc --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/SearchHistoryAdapter.kt @@ -0,0 +1,51 @@ +package campus.tech.kakao.map.presentation + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.databinding.SearchHistoryItemBinding + +class SearchHistoryAdapter( + private var historyList: MutableList, + private val onDeleteClick: (String) -> Unit, + private val onItemClick: (String) -> Unit, +) : RecyclerView.Adapter() { + class HistoryViewHolder(private val binding: SearchHistoryItemBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: String, onDeleteClick: (String) -> Unit, onItemClick: (String) -> Unit) { + binding.history = item + binding.clickListener = View.OnClickListener { + onItemClick(item) + } + binding.deleteListener = View.OnClickListener { + onDeleteClick(item) + } + binding.executePendingBindings() + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): HistoryViewHolder { + val binding = SearchHistoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return HistoryViewHolder(binding) + } + + override fun getItemCount(): Int { + return historyList.size + } + + override fun onBindViewHolder( + holder: HistoryViewHolder, + position: Int, + ) { + holder.bind(historyList[position], onDeleteClick, onItemClick) + } + + fun updateData(newHistoryList: List) { + historyList.clear() + historyList.addAll(newHistoryList) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/utils/ApiKeyProvider.kt b/app/src/main/java/campus/tech/kakao/map/utils/ApiKeyProvider.kt new file mode 100644 index 00000000..c8fd7a58 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/utils/ApiKeyProvider.kt @@ -0,0 +1,9 @@ +package campus.tech.kakao.map.utils + +import campus.tech.kakao.map.BuildConfig + + +object ApiKeyProvider { + val KAKAO_REST_API_KEY = "KakaoAK " + BuildConfig.KAKAO_REST_API_KEY + val KAKAO_API_KEY = BuildConfig.KAKAO_API_KEY +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_sheet_background.xml b/app/src/main/res/drawable/bottom_sheet_background.xml new file mode 100644 index 00000000..f79045b0 --- /dev/null +++ b/app/src/main/res/drawable/bottom_sheet_background.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/cancel.xml b/app/src/main/res/drawable/cancel.xml new file mode 100644 index 00000000..08e33364 --- /dev/null +++ b/app/src/main/res/drawable/cancel.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/location.xml b/app/src/main/res/drawable/location.xml new file mode 100644 index 00000000..a909fdb8 --- /dev/null +++ b/app/src/main/res/drawable/location.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/marker.png b/app/src/main/res/drawable/marker.png new file mode 100644 index 00000000..b2327fcd Binary files /dev/null and b/app/src/main/res/drawable/marker.png differ diff --git a/app/src/main/res/drawable/rounded_border.xml b/app/src/main/res/drawable/rounded_border.xml new file mode 100644 index 00000000..c5572c97 --- /dev/null +++ b/app/src/main/res/drawable/rounded_border.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/drawable/search_icon.png b/app/src/main/res/drawable/search_icon.png new file mode 100644 index 00000000..bf36b9fd Binary files /dev/null and b/app/src/main/res/drawable/search_icon.png differ diff --git a/app/src/main/res/layout/activity_error.xml b/app/src/main/res/layout/activity_error.xml new file mode 100644 index 00000000..9493a025 --- /dev/null +++ b/app/src/main/res/layout/activity_error.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml new file mode 100644 index 00000000..e42378b8 --- /dev/null +++ b/app/src/main/res/layout/activity_map.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_place.xml b/app/src/main/res/layout/activity_place.xml new file mode 100644 index 00000000..87cbe640 --- /dev/null +++ b/app/src/main/res/layout/activity_place.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/place_item.xml b/app/src/main/res/layout/place_item.xml new file mode 100644 index 00000000..a8b0aca3 --- /dev/null +++ b/app/src/main/res/layout/place_item.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/search_history_item.xml b/app/src/main/res/layout/search_history_item.xml new file mode 100644 index 00000000..f6161db4 --- /dev/null +++ b/app/src/main/res/layout/search_history_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..7b8d0a3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Map + 지도 인증을 실패 했습니다. \n 다시 시도해 주세요. \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2927e499..549c1abd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jun 15 19:44:23 KST 2024 +#Mon Jul 22 17:49:11 KST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists