From 326609771e7ff497d7fccf0aaa667d26d117ec4b Mon Sep 17 00:00:00 2001 From: yb0x00 Date: Tue, 30 Jul 2024 15:43:36 +0900 Subject: [PATCH 1/4] =?UTF-8?q?5=EC=A3=BC=EC=B0=A8=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기본 코드 준비 --- app/build.gradle.kts | 25 ++- .../tech/kakao/map/DataSearchActivityTest.kt | 26 +++ .../tech/kakao/map/HomeMapActivityTest.kt | 65 +++++++ .../tech/kakao/map/MapErrorActivityTest.kt | 33 ++++ app/src/main/AndroidManifest.xml | 13 +- .../java/campus/tech/kakao/map/Application.kt | 16 ++ .../campus/tech/kakao/map/MainActivity.kt | 11 -- .../kakao/map/adapter/RecentSearchAdapter.kt | 50 ++++++ .../kakao/map/adapter/SearchDataAdapter.kt | 64 +++++++ .../adapter/listener/RecentAdapterListener.kt | 5 + .../adapter/listener/SearchAdapterListener.kt | 5 + .../kakao/map/data/LocationDataContract.kt | 8 + .../kakao/map/data/room/DatabaseModule.kt | 25 +++ .../kakao/map/data/room/SearchHisotryData.kt | 13 ++ .../kakao/map/data/room/SearchHistoryDao.kt | 26 +++ .../map/data/room/SearchHistoryDatabase.kt | 30 ++++ .../map/repository/PreferenceRepository.kt | 24 +++ .../map/repository/SearchHistoryRepository.kt | 55 ++++++ .../map/repository/SearchResultRepository.kt | 37 ++++ .../tech/kakao/map/retrofit/Category.kt | 25 +++ .../kakao/map/retrofit/MapSearchResponse.kt | 19 +++ .../tech/kakao/map/retrofit/RetrofitAPI.kt | 45 +++++ .../kakao/map/retrofit/RetrofitService.kt | 14 ++ .../tech/kakao/map/view/DataSearchActivity.kt | 119 +++++++++++++ .../tech/kakao/map/view/HomeMapActivity.kt | 158 ++++++++++++++++++ .../tech/kakao/map/view/MapErrorActivity.kt | 52 ++++++ .../kakao/map/viewModel/MapErrorViewModel.kt | 19 +++ .../tech/kakao/map/viewModel/MapViewModel.kt | 33 ++++ .../map/viewModel/SearchHistoryViewModel.kt | 47 ++++++ .../kakao/map/viewModel/SearchViewModel.kt | 28 ++++ app/src/main/res/drawable/loading_icon.xml | 13 ++ .../main/res/drawable/location_map_icon.xml | 9 + app/src/main/res/drawable/map_label.png | Bin 0 -> 8726 bytes app/src/main/res/drawable/no_search.xml | 7 + app/src/main/res/drawable/reload_icon.xml | 12 ++ app/src/main/res/drawable/search_icon.xml | 10 ++ .../main/res/layout/activity_data_search.xml | 80 +++++++++ app/src/main/res/layout/activity_home_map.xml | 59 +++++++ app/src/main/res/layout/activity_main.xml | 19 --- .../main/res/layout/activity_map_error.xml | 68 ++++++++ .../res/layout/map_detail_bottom_sheet.xml | 46 +++++ .../main/res/layout/recent_search_item.xml | 43 +++++ app/src/main/res/layout/search_data_item.xml | 76 +++++++++ app/src/main/res/values/mycolors.xml | 5 + app/src/main/res/values/mystrings.xml | 10 ++ .../tech/kakao/map/MapErrorViewModelTest.kt | 36 ++++ build.gradle.kts | 2 +- 47 files changed, 1546 insertions(+), 39 deletions(-) create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/DataSearchActivityTest.kt create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/HomeMapActivityTest.kt create mode 100644 app/src/androidTest/java/campus/tech/kakao/map/MapErrorActivityTest.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/Application.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/adapter/RecentSearchAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/adapter/SearchDataAdapter.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/adapter/listener/RecentAdapterListener.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/adapter/listener/SearchAdapterListener.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/LocationDataContract.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/room/DatabaseModule.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/room/SearchHisotryData.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDao.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDatabase.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/PreferenceRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/SearchHistoryRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/SearchResultRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/retrofit/Category.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/retrofit/MapSearchResponse.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitAPI.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitService.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/DataSearchActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/HomeMapActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/MapErrorActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewModel/MapErrorViewModel.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/SearchHistoryViewModel.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewModel/SearchViewModel.kt create mode 100644 app/src/main/res/drawable/loading_icon.xml create mode 100644 app/src/main/res/drawable/location_map_icon.xml create mode 100644 app/src/main/res/drawable/map_label.png create mode 100644 app/src/main/res/drawable/no_search.xml create mode 100644 app/src/main/res/drawable/reload_icon.xml create mode 100644 app/src/main/res/drawable/search_icon.xml create mode 100644 app/src/main/res/layout/activity_data_search.xml create mode 100644 app/src/main/res/layout/activity_home_map.xml delete mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_map_error.xml create mode 100644 app/src/main/res/layout/map_detail_bottom_sheet.xml create mode 100644 app/src/main/res/layout/recent_search_item.xml create mode 100644 app/src/main/res/layout/search_data_item.xml create mode 100644 app/src/main/res/values/mycolors.xml create mode 100644 app/src/main/res/values/mystrings.xml create mode 100644 app/src/test/java/campus/tech/kakao/map/MapErrorViewModelTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 803085bd..51bb607d 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") @@ -5,7 +7,7 @@ plugins { id("kotlin-parcelize") id("kotlin-kapt") id("com.google.dagger.hilt.android") - id("com.google.gms.google-services") + //id("com.google.gms.google-services") } android { @@ -13,6 +15,14 @@ android { compileSdk = 34 defaultConfig { + + ndk { + abiFilters.add("arm64-v8a") + abiFilters.add("armeabi-v7a") + abiFilters.add("x86") + abiFilters.add("x86_64") + } + applicationId = "campus.tech.kakao.map" minSdk = 26 targetSdk = 34 @@ -20,6 +30,9 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY")) + buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY")) } buildTypes { @@ -45,6 +58,8 @@ android { } } +fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key) + dependencies { implementation("androidx.core:core-ktx:1.13.1") @@ -64,10 +79,10 @@ dependencies { kapt("com.google.dagger:hilt-compiler:2.48.1") implementation("androidx.activity:activity-ktx:1.9.0") implementation("androidx.room:room-ktx:2.6.1") - implementation(platform("com.google.firebase:firebase-bom:33.1.2")) - implementation("com.google.firebase:firebase-analytics-ktx") - implementation("com.google.firebase:firebase-config-ktx:22.0.0") - implementation("com.google.firebase:firebase-messaging-ktx:24.0.0") + //implementation(platform("com.google.firebase:firebase-bom:33.1.2")) + //implementation("com.google.firebase:firebase-analytics-ktx") + //implementation("com.google.firebase:firebase-config-ktx:22.0.0") + //implementation("com.google.firebase:firebase-messaging-ktx:24.0.0") testImplementation("androidx.room:room-testing:2.6.1") testImplementation("junit:junit:4.13.2") testImplementation("io.mockk:mockk-android:1.13.11") diff --git a/app/src/androidTest/java/campus/tech/kakao/map/DataSearchActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/DataSearchActivityTest.kt new file mode 100644 index 00000000..32b5f677 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/DataSearchActivityTest.kt @@ -0,0 +1,26 @@ +package campus.tech.kakao.map + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import campus.tech.kakao.map.view.DataSearchActivity +import org.junit.Rule +import org.junit.Test + + +class DataSearchActivityTest { + @get:Rule + val activityRule = ActivityScenarioRule(DataSearchActivity::class.java) + + @Test + fun 검색어_입력시_검색_결과가_나타남() { + onView(withId(R.id.searchBar)) + .perform(replaceText("전남대학교"), closeSoftKeyboard()) + onView(withId(R.id.searchResulListView)) + .check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/HomeMapActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/HomeMapActivityTest.kt new file mode 100644 index 00000000..2c116de1 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/HomeMapActivityTest.kt @@ -0,0 +1,65 @@ +package campus.tech.kakao.map + +import android.content.Intent +import androidx.test.core.app.ApplicationProvider +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.init +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.Intents.release +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import campus.tech.kakao.map.view.DataSearchActivity +import campus.tech.kakao.map.view.HomeMapActivity +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class HomeMapActivityTest { + @get:Rule + val activityRule: ActivityScenarioRule = + ActivityScenarioRule( + Intent( + ApplicationProvider.getApplicationContext(), + HomeMapActivity::class.java + ).apply { + putExtra("name", "전남대학교 여수캠퍼스") + putExtra("address", "전라남도 여수시 대학로 50") + putExtra("latitude", "34.772884563341") + putExtra("longitude", "127.69954169563") + } + ) + + @Before + fun setup() { + init() + } + + @After + fun teardown() { + release() + } + + @Test + fun 목록에서_선택한_장소의_정보가_BottomSheet에_나타남() { + onView(withId(R.id.placeName)) + .check(matches(withText("전남대학교 여수캠퍼스"))) + onView(withId(R.id.placeAddress)) + .check(matches(withText("전라남도 여수시 대학로 50"))) + onView(withId(R.id.bottomSheet)) + .check(matches(isDisplayed())) + } + + @Test + fun 검색바를_클릭하면_검색화면으로_이동() { + onView(withId(R.id.searchbar_home)) + .perform(click()) + intended(hasComponent(DataSearchActivity::class.java.name)) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MapErrorActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MapErrorActivityTest.kt new file mode 100644 index 00000000..e8880f4b --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MapErrorActivityTest.kt @@ -0,0 +1,33 @@ +package campus.tech.kakao.map + +import android.content.Intent +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +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.view.HomeMapActivity +import campus.tech.kakao.map.view.MapErrorActivity +import org.junit.Rule +import org.junit.Test + + +class MapErrorActivityTest { + @get:Rule + val activityRule : ActivityScenarioRule = + ActivityScenarioRule( + Intent( + ApplicationProvider.getApplicationContext(), + MapErrorActivity::class.java + ).apply { + putExtra("errorMessage","java.lang.Exception:401") + } + ) + + @Test + fun onMapError_예외설명_에러화면에_나타남() { + onView(withId(R.id.error_detail)) + .check(matches(withText("401"))) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f2e7b45a..1f358101 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,11 +2,10 @@ - - + @@ -25,6 +28,10 @@ + diff --git a/app/src/main/java/campus/tech/kakao/map/Application.kt b/app/src/main/java/campus/tech/kakao/map/Application.kt new file mode 100644 index 00000000..ea681f61 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Application.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map + +import android.app.Application +import com.kakao.vectormap.KakaoMapSdk +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class Application : Application() { + override fun onCreate(){ + super.onCreate() + + //카카오맵 SDK 초기화 + val key = getString(R.string.kakao_api_key) + KakaoMapSdk.init(this, 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/adapter/RecentSearchAdapter.kt b/app/src/main/java/campus/tech/kakao/map/adapter/RecentSearchAdapter.kt new file mode 100644 index 00000000..81f2c840 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/RecentSearchAdapter.kt @@ -0,0 +1,50 @@ +package campus.tech.kakao.map.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.data.room.SearchHistoryData +import campus.tech.kakao.map.adapter.listener.RecentAdapterListener +import campus.tech.kakao.map.viewModel.SearchHistoryViewModel +import kotlinx.coroutines.launch + +class RecentSearchAdapter( + private val searchHistoryDataList: List, + private val searchHistoryViewModel: SearchHistoryViewModel, + private val adapterListener: RecentAdapterListener +) : RecyclerView.Adapter() { + + class RecentViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val name: TextView = view.findViewById(R.id.recent_search) + val deleteBtn: ImageButton = view.findViewById(R.id.delete_recent) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.recent_search_item, parent, false) + return RecentViewHolder(view) + } + + override fun getItemCount(): Int = searchHistoryDataList.count() + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = searchHistoryDataList[position] + val viewHolder = holder as RecentViewHolder + viewHolder.name.text = item.name + + viewHolder.deleteBtn.setOnClickListener { + searchHistoryViewModel.viewModelScope.launch { + searchHistoryViewModel.deleteRecentData(item.name, item.address) + } + } + + viewHolder.name.setOnClickListener { + adapterListener.autoSearch(item.name) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/adapter/SearchDataAdapter.kt b/app/src/main/java/campus/tech/kakao/map/adapter/SearchDataAdapter.kt new file mode 100644 index 00000000..541bbe59 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/SearchDataAdapter.kt @@ -0,0 +1,64 @@ +package campus.tech.kakao.map.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.adapter.listener.SearchAdapterListener +import campus.tech.kakao.map.viewModel.SearchHistoryViewModel +import campus.tech.kakao.map.retrofit.Document +import kotlinx.coroutines.launch + +class SearchDataAdapter( + private var items: List, + private val recentViewModel: SearchHistoryViewModel, + private var adapterListener: SearchAdapterListener +) : RecyclerView.Adapter() { + + class SearchDataViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val name: TextView = view.findViewById(R.id.search_data_name) + val address: TextView = view.findViewById(R.id.search_data_address) + val category: TextView = view.findViewById(R.id.search_data_category) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.search_data_item, parent, false) + return SearchDataViewHolder(view) + } + + override fun getItemCount(): Int = items.count() + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + val viewHolder = holder as SearchDataViewHolder + viewHolder.name.text = item.name + viewHolder.address.text = item.address + // API 데이터에 "category_group_code"가 있는 경우 해당 그룹 코드에 맞는 설명을 출력하고, 비어 있는 경우 "category_name"의 일부 문자열을 출력 + if (item.categoryCode.isEmpty()) { + viewHolder.category.text = item.categoryTail + } else { + viewHolder.category.text = item.categoryDescription + } + holder.itemView.setOnClickListener { + val searchTime = System.currentTimeMillis() + recentViewModel.viewModelScope.launch { + recentViewModel.addRecentSearchItem(item.name, item.address, searchTime) + } + adapterListener.displaySearchLocation( + item.name, + item.address, + item.latitude, + item.longitude + ) + } + } + + fun updateData(newItems: List) { + items = newItems + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/adapter/listener/RecentAdapterListener.kt b/app/src/main/java/campus/tech/kakao/map/adapter/listener/RecentAdapterListener.kt new file mode 100644 index 00000000..bfc8a193 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/listener/RecentAdapterListener.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.adapter.listener + +interface RecentAdapterListener { + fun autoSearch(searchData: String) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/adapter/listener/SearchAdapterListener.kt b/app/src/main/java/campus/tech/kakao/map/adapter/listener/SearchAdapterListener.kt new file mode 100644 index 00000000..70d54690 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/adapter/listener/SearchAdapterListener.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.adapter.listener + +interface SearchAdapterListener { + fun displaySearchLocation(name: String, address: String, latitude: String, longitude: String) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/LocationDataContract.kt b/app/src/main/java/campus/tech/kakao/map/data/LocationDataContract.kt new file mode 100644 index 00000000..07127117 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/LocationDataContract.kt @@ -0,0 +1,8 @@ +package campus.tech.kakao.map.data + +object LocationDataContract { + const val LOCATION_NAME = "name" + const val LOCATION_ADDRESS = "address" + const val LOCATION_LATITUDE = "latitude" + const val LOCATION_LONGITUDE = "longitude" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/room/DatabaseModule.kt b/app/src/main/java/campus/tech/kakao/map/data/room/DatabaseModule.kt new file mode 100644 index 00000000..49c8bb0d --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/room/DatabaseModule.kt @@ -0,0 +1,25 @@ +package campus.tech.kakao.map.data.room + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import android.content.Context + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun provideSearchHistoryDatabase(@ApplicationContext context: Context): SearchHistoryDatabase { + return SearchHistoryDatabase.getDatabase(context) + } + + @Provides + fun provideSearchHistoryDao(database: SearchHistoryDatabase): SearchHistoryDao { + return database.searchHistoryDao() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/room/SearchHisotryData.kt b/app/src/main/java/campus/tech/kakao/map/data/room/SearchHisotryData.kt new file mode 100644 index 00000000..96616cd7 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/room/SearchHisotryData.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.data.room + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "searchHistory") +data class SearchHistoryData( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo val name: String, + @ColumnInfo val address: String, + @ColumnInfo val searchTime: Long +) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDao.kt b/app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDao.kt new file mode 100644 index 00000000..2aa9e574 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDao.kt @@ -0,0 +1,26 @@ +package campus.tech.kakao.map.data.room + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import javax.inject.Singleton + +@Singleton +@Dao +interface SearchHistoryDao { + @Query("SELECT * FROM searchHistory ORDER BY searchTime DESC") + suspend fun getSearchHistory(): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertSearchHistory(vararg searchHistory: SearchHistoryData) + + @Query("DELETE FROM searchHistory WHERE name = :name AND address = :address") + suspend fun deleteSearchItem(name: String, address: String) + + @Query("UPDATE searchHistory SET searchTime = :newTime WHERE name = :name AND address = :address") + suspend fun updateSearchTime(name: String, address: String, newTime: Long) + + @Query("SELECT * FROM searchHistory WHERE name = :name AND address = :address") + suspend fun findSearchItem(name: String, address: String): SearchHistoryData? +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDatabase.kt b/app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDatabase.kt new file mode 100644 index 00000000..05c4ac75 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/room/SearchHistoryDatabase.kt @@ -0,0 +1,30 @@ +package campus.tech.kakao.map.data.room + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [SearchHistoryData::class], version = 1, exportSchema = false) +abstract class SearchHistoryDatabase : RoomDatabase() { + + abstract fun searchHistoryDao(): SearchHistoryDao + + companion object { + @Volatile + private var INSTANCE: SearchHistoryDatabase? = null + + fun getDatabase(context: Context): SearchHistoryDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + SearchHistoryDatabase::class.java, + "searchHistory_database" + ).fallbackToDestructiveMigration() + .build() + INSTANCE = instance + return instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/PreferenceRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/PreferenceRepository.kt new file mode 100644 index 00000000..37183840 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/PreferenceRepository.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.repository + +import android.content.Context +import android.content.SharedPreferences +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PreferenceRepository @Inject constructor( + @ApplicationContext context: Context +) { + + private var sharedPrefs: SharedPreferences = + context.getSharedPreferences("location_data", Context.MODE_PRIVATE) + + fun setString(key: String, value: String) { + sharedPrefs.edit().putString(key, value).apply() + } + + fun getString(key: String, value: String?): String { + return sharedPrefs.getString(key, value).toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/SearchHistoryRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SearchHistoryRepository.kt new file mode 100644 index 00000000..f4561c60 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/SearchHistoryRepository.kt @@ -0,0 +1,55 @@ +package campus.tech.kakao.map.repository + +import campus.tech.kakao.map.data.room.SearchHistoryDao +import campus.tech.kakao.map.data.room.SearchHistoryData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SearchHistoryRepository @Inject constructor( + private val searchHistoryDao: SearchHistoryDao +) { + suspend fun getSearchHistory(): List { + return withContext(Dispatchers.IO) { + searchHistoryDao.getSearchHistory() + } + } + + suspend fun insertSearchData(name: String, address: String, time: Long) { + withContext(Dispatchers.IO) { + val data = mappingData(name, address, time) + searchHistoryDao.insertSearchHistory(data) + } + } + + suspend fun deleteSearchData(name: String, address: String) { + withContext(Dispatchers.IO) { + val item = searchHistoryDao.findSearchItem(name, address) + if (item != null) { + searchHistoryDao.deleteSearchItem(name, address) + } + } + } + + suspend fun updateTime(name: String, address: String, time: Long) { + withContext(Dispatchers.IO) { + searchHistoryDao.updateSearchTime(name, address, time) + } + } + + suspend fun findSearchItem(name: String, address: String): SearchHistoryData? { + return withContext(Dispatchers.IO) { + searchHistoryDao.findSearchItem(name, address) + } + } + + private fun mappingData(name: String, address: String, time: Long): SearchHistoryData { + return SearchHistoryData( + name = name, + address = address, + searchTime = time + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/SearchResultRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SearchResultRepository.kt new file mode 100644 index 00000000..9ae31249 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/SearchResultRepository.kt @@ -0,0 +1,37 @@ +package campus.tech.kakao.map.repository + +import campus.tech.kakao.map.retrofit.CategoryData +import campus.tech.kakao.map.retrofit.Document +import campus.tech.kakao.map.retrofit.RetrofitAPI +import javax.inject.Inject + +class SearchResultRepository @Inject constructor( + private val retrofitAPI: RetrofitAPI +) { + + //검색 결과 가공 + fun loadResultMapData(data: String, callback: (List) -> Unit) { + retrofitAPI.getResultFromAPI(data) { response -> + if (response.isSuccessful) { + val body = response.body() + body?.documents?.let { documents -> + val updatedDocuments = documents.map { document -> + val descriptionFromCode = CategoryData.descriptions[document.categoryCode] + val descriptionFromCategory = getTailCategory(document.category) + document.copy( + categoryDescription = descriptionFromCode, + categoryTail = descriptionFromCategory + ) + } + callback(updatedDocuments) + } + } + } + } + + //API 데이터에서 "category_name" 문자열이 길기 때문에, 충분한 의미 전달이 되는 키워드를 임의로 추출 + private fun getTailCategory(category: String): String { + val parts = category.split(">") + return if (parts.size > 1) parts[1].trim() else parts.last().trim() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/retrofit/Category.kt b/app/src/main/java/campus/tech/kakao/map/retrofit/Category.kt new file mode 100644 index 00000000..4d74236d --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/retrofit/Category.kt @@ -0,0 +1,25 @@ +package campus.tech.kakao.map.retrofit + +// "category_group_code"와 설명을 매칭하기 위한 객체 +object CategoryData { + val descriptions = mapOf( + "MT1" to "대형마트", + "CS2" to "편의점", + "PS3" to "어린이집, 유치원", + "SC4" to "학교", + "AC5" to "학원", + "PK6" to "주차장", + "OL7" to "주유소, 충전소", + "SW8" to "지하철역", + "BK9" to "은행", + "CT1" to "문화시설", + "AG2" to "중개업소", + "PO3" to "공공기관", + "AT4" to "관광명소", + "AD5" to "숙박", + "FD6" to "음식점", + "CE7" to "카페", + "HP8" to "병원", + "PM9" to "약국" + ) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/retrofit/MapSearchResponse.kt b/app/src/main/java/campus/tech/kakao/map/retrofit/MapSearchResponse.kt new file mode 100644 index 00000000..75f3d734 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/retrofit/MapSearchResponse.kt @@ -0,0 +1,19 @@ +package campus.tech.kakao.map.retrofit + +import com.google.gson.annotations.SerializedName + +data class Document( + @SerializedName("place_name") val name: String, + @SerializedName("category_name") val category: String, + @SerializedName("address_name") val address: String, + @SerializedName("category_group_code") val categoryCode: String, + @SerializedName("x") val longitude: String, + @SerializedName("y") val latitude: String, + var categoryDescription: String? = null, + var categoryTail: String? = null +) + +data class MapSearchResponse( + @SerializedName("documents") val documents: List +) + diff --git a/app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitAPI.kt b/app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitAPI.kt new file mode 100644 index 00000000..df4b676f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitAPI.kt @@ -0,0 +1,45 @@ +package campus.tech.kakao.map.retrofit + +import android.util.Log +import campus.tech.kakao.map.BuildConfig +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RetrofitAPI @Inject constructor() { + //실제 사용될 때 Retrofit 객체 생성 + private val retrofitService: RetrofitService by lazy { + Log.d("yeong", "Use Retrofit!") + Retrofit.Builder() + .baseUrl("https://dapi.kakao.com/v2/local/search/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(RetrofitService::class.java) + } + + //카카오맵 API 검색 결과 + fun getResultFromAPI(keyword: String, callback: (Response) -> Unit) { + val apiKey = "KakaoAK ${BuildConfig.KAKAO_REST_API_KEY}" + + retrofitService.requestResults(apiKey, keyword) + .enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + callback(response) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("API", "error : $t") + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitService.kt b/app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitService.kt new file mode 100644 index 00000000..1315bf19 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/retrofit/RetrofitService.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map.retrofit + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface RetrofitService { + @GET("keyword.json") + fun requestResults( + @Header("Authorization") apiKey: String, + @Query("query") query: String + ): Call +} diff --git a/app/src/main/java/campus/tech/kakao/map/view/DataSearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/DataSearchActivity.kt new file mode 100644 index 00000000..13c82349 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/DataSearchActivity.kt @@ -0,0 +1,119 @@ +package campus.tech.kakao.map.view + +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import campus.tech.kakao.map.adapter.listener.RecentAdapterListener +import campus.tech.kakao.map.adapter.listener.SearchAdapterListener +import campus.tech.kakao.map.adapter.RecentSearchAdapter +import campus.tech.kakao.map.adapter.SearchDataAdapter +import campus.tech.kakao.map.data.LocationDataContract +import campus.tech.kakao.map.databinding.ActivityDataSearchBinding +import campus.tech.kakao.map.viewModel.SearchHistoryViewModel +import campus.tech.kakao.map.viewModel.SearchViewModel +import dagger.hilt.android.AndroidEntryPoint +import androidx.activity.viewModels + +@AndroidEntryPoint +class DataSearchActivity : AppCompatActivity(), RecentAdapterListener, SearchAdapterListener { + private lateinit var binding: ActivityDataSearchBinding + + private val searchViewModel: SearchViewModel by viewModels() + private val searchHistoryViewModel: SearchHistoryViewModel by viewModels() //최근 검색어 관리 + + private lateinit var searchResultDataAdapter: SearchDataAdapter //검색 결과 표시 위함 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setBinding() + + //RecyclerView Layout 매니저 설정 (스크롤 방향 설정) + binding.searchResulListView.layoutManager = LinearLayoutManager(this) + binding.recentSearchListView.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + + //Adapter 초기화 + searchResultDataAdapter = SearchDataAdapter(emptyList(), searchHistoryViewModel, this) + binding.searchResulListView.adapter = searchResultDataAdapter + binding.recentSearchListView.adapter = + RecentSearchAdapter(emptyList(), searchHistoryViewModel, this) + + resetButtonListener() //x버튼 눌러 검색 입력 필드 내용 삭제 + setTextWatcher() //검색 입력 필드 텍스트 변경 리스너 + + //검색 결과 데이터 관찰 -> searchResultAdapter 데이터 넘기기 + searchViewModel.getSearchDataLiveData().observe(this, Observer { documentsList -> + if (documentsList.isNotEmpty()) { + binding.noResult.visibility = View.GONE + searchResultDataAdapter.updateData(documentsList) + } else { + binding.noResult.visibility = View.VISIBLE + searchResultDataAdapter.updateData(emptyList()) + } + }) + + //검색 history 데이터 관찰 + searchHistoryViewModel.getRecentDataLiveData().observe(this, Observer { recentData -> + binding.recentSearchListView.adapter = + RecentSearchAdapter(recentData, searchHistoryViewModel, this) + }) + } + + private fun setBinding() { + binding = ActivityDataSearchBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + } + + private fun setTextWatcher() { + binding.searchBar.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val searchInput = binding.searchBar.text.trim().toString() + + if (searchInput.isNotEmpty()) { + searchViewModel.loadResultData(searchInput) + } else { + binding.noResult.visibility = View.VISIBLE + searchResultDataAdapter.updateData(emptyList()) + } + } + + override fun afterTextChanged(s: Editable?) { + } + }) + } + + private fun resetButtonListener() { + binding.deleteInput.setOnClickListener { + binding.searchBar.text.clear() + } + } + + //클릭한 검색어가 자동으로 입력되는 기능 구현 + override fun autoSearch(searchData: String) { + binding.searchBar.setText(searchData) + } + + override fun displaySearchLocation( + name: String, + address: String, + latitude: String, + longitude: String + ) { + val intent = Intent(this@DataSearchActivity, HomeMapActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(LocationDataContract.LOCATION_NAME, name) + intent.putExtra(LocationDataContract.LOCATION_ADDRESS, address) + intent.putExtra(LocationDataContract.LOCATION_LATITUDE, latitude) + intent.putExtra(LocationDataContract.LOCATION_LONGITUDE, longitude) + startActivity(intent) + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/view/HomeMapActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/HomeMapActivity.kt new file mode 100644 index 00000000..f5679adc --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/HomeMapActivity.kt @@ -0,0 +1,158 @@ +package campus.tech.kakao.map.view + +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import campus.tech.kakao.map.R +import campus.tech.kakao.map.data.LocationDataContract +import campus.tech.kakao.map.databinding.ActivityHomeMapBinding +import campus.tech.kakao.map.viewModel.MapViewModel +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.kakao.vectormap.KakaoMap +import com.kakao.vectormap.KakaoMapReadyCallback +import com.kakao.vectormap.LatLng +import com.kakao.vectormap.MapLifeCycleCallback +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles +import dagger.hilt.android.AndroidEntryPoint +import androidx.activity.viewModels +import androidx.databinding.DataBindingUtil +import campus.tech.kakao.map.databinding.MapDetailBottomSheetBinding + +@AndroidEntryPoint +class HomeMapActivity : AppCompatActivity() { + private lateinit var binding: ActivityHomeMapBinding + private lateinit var bottomSheetBinding: MapDetailBottomSheetBinding + + private val mapViewModel: MapViewModel by viewModels() + + private val bottomSheet: ConstraintLayout by lazy { findViewById(R.id.bottomSheet) } + private lateinit var bottomBehavior: BottomSheetBehavior + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setBinding() + setViewModel() + + val placeName = intent.getStringExtra(LocationDataContract.LOCATION_NAME).toString() + val placeAddress = intent.getStringExtra(LocationDataContract.LOCATION_ADDRESS).toString() + val placeLatitude = + intent.getStringExtra(LocationDataContract.LOCATION_LATITUDE)?.toDouble() + val placeLongitude = + intent.getStringExtra(LocationDataContract.LOCATION_LONGITUDE)?.toDouble() + + bottomBehavior = BottomSheetBehavior.from(bottomSheet) + + //KaKao Map UI에 띄우기 + binding.mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() { + } + + override fun onMapError(p0: Exception?) { + setErrorIntent(p0) + } + + }, object : KakaoMapReadyCallback() { + override fun onMapReady(p0: KakaoMap) { + + // 라벨 생성 + if (placeLatitude != null && placeLongitude != null) { + p0.labelManager + val style = + p0.labelManager?.addLabelStyles( + LabelStyles.from( + LabelStyle.from(R.drawable.map_label).setTextStyles( + 35, + Color.BLACK, 1, Color.RED + ) + ) + ) + val options = + LabelOptions.from(LatLng.from(placeLatitude, placeLongitude)) + .setStyles(style) + .setTexts(placeName) + val layer = p0.labelManager?.layer + layer?.addLabel(options) + } + } + + // 지도 시작 시 위치 좌표 설정 + override fun getPosition(): LatLng { + val savedLatitude = + mapViewModel.getLocation(LocationDataContract.LOCATION_LATITUDE, null) + .toDoubleOrNull() + val savedLongitude = + mapViewModel.getLocation(LocationDataContract.LOCATION_LONGITUDE, null) + .toDoubleOrNull() + + return if (placeLatitude != null && placeLongitude != null) { + LatLng.from(placeLatitude, placeLongitude) + } else if (savedLatitude != null && savedLongitude != null) { + LatLng.from(savedLatitude, savedLongitude) + } else { + super.getPosition() + } + } + }) + + if (placeLatitude != null && placeLongitude != null) { + bottomBehavior.state = BottomSheetBehavior.STATE_EXPANDED + mapViewModel.updateInfo(placeName, placeAddress) + } else { + bottomBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + binding.searchbarHome.setOnClickListener { + val intent = Intent(this, DataSearchActivity::class.java) + startActivity(intent) + } + } + + override fun onResume() { + super.onResume() + binding.mapView.resume() + } + + override fun onPause() { + super.onPause() + binding.mapView.pause() + } + + override fun onDestroy() { + super.onDestroy() + intent.getStringExtra(LocationDataContract.LOCATION_LATITUDE) + ?.let { mapViewModel.saveLocation(LocationDataContract.LOCATION_LATITUDE, it) } + intent.getStringExtra(LocationDataContract.LOCATION_LONGITUDE) + ?.let { mapViewModel.saveLocation(LocationDataContract.LOCATION_LONGITUDE, it) } + } + + private fun setErrorIntent(errorMsg: Exception?) { + val intentError = Intent(this@HomeMapActivity, MapErrorActivity::class.java) + intentError.putExtra("ERROR_MESSAGE", errorMsg.toString()) + startActivity(intentError) + } + + private fun setBinding() { + binding = ActivityHomeMapBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + bottomSheetBinding = DataBindingUtil.inflate( + layoutInflater, + R.layout.map_detail_bottom_sheet, + findViewById(R.id.bottomSheet), + true + ) + bottomSheetBinding.lifecycleOwner = this + } + + private fun setViewModel() { + bottomSheetBinding.viewModel = mapViewModel + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/MapErrorActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/MapErrorActivity.kt new file mode 100644 index 00000000..f8ec5195 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/MapErrorActivity.kt @@ -0,0 +1,52 @@ +package campus.tech.kakao.map.view + +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivityMapErrorBinding +import campus.tech.kakao.map.viewModel.MapErrorViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MapErrorActivity : AppCompatActivity() { + private lateinit var binding: ActivityMapErrorBinding + private lateinit var viewModel: MapErrorViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setBinding() + setViewModel() + setErrorMsg() + clickBackBtn() + } + + private fun setBinding() { + binding = DataBindingUtil.setContentView(this, R.layout.activity_map_error) + binding.activity = this + } + + private fun setViewModel() { + viewModel = ViewModelProvider(this)[MapErrorViewModel::class.java] + binding.lifecycleOwner = this + binding.viewModel = viewModel + } + + private fun setErrorMsg() { + val errorMsg = intent.getStringExtra("ERROR_MESSAGE") + viewModel.setErrorMsg(errorMsg) + } + + private fun clickBackBtn() { + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finishAffinity() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewModel/MapErrorViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewModel/MapErrorViewModel.kt new file mode 100644 index 00000000..19c99d71 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewModel/MapErrorViewModel.kt @@ -0,0 +1,19 @@ +package campus.tech.kakao.map.viewModel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class MapErrorViewModel @Inject constructor() : ViewModel() { + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData get() = _errorMessage + + fun setErrorMsg(error: String?) { + if (error != null) { + _errorMessage.value = error.substringAfterLast(":").trim() + } + } +} \ 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..fadc0f16 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewModel/MapViewModel.kt @@ -0,0 +1,33 @@ +package campus.tech.kakao.map.viewModel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import campus.tech.kakao.map.repository.PreferenceRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class MapViewModel @Inject constructor( + private val prefersRepo: PreferenceRepository +) : ViewModel() { + + private val _placeInfoList = MutableLiveData>() + val placeInfoList: LiveData> get() = _placeInfoList + + init { + _placeInfoList.value = listOf("NONE", "NONE") + } + + fun updateInfo(name: String, address: String) { + _placeInfoList.value = listOf(name, address) + } + + fun saveLocation(locationKey: String, data: String) { + prefersRepo.setString(locationKey, data) + } + + fun getLocation(locationKey: String, data: String?): String { + return prefersRepo.getString(locationKey, data) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewModel/SearchHistoryViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewModel/SearchHistoryViewModel.kt new file mode 100644 index 00000000..b6ce161e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewModel/SearchHistoryViewModel.kt @@ -0,0 +1,47 @@ +package campus.tech.kakao.map.viewModel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.data.room.SearchHistoryData +import campus.tech.kakao.map.repository.SearchHistoryRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchHistoryViewModel @Inject constructor( + private val searchHistoryRepo: SearchHistoryRepository +) : ViewModel() { + + private val _searchHistoryDataList = MutableLiveData>() + private val searchHistoryDataList: LiveData> = _searchHistoryDataList + + init { + viewModelScope.launch { + _searchHistoryDataList.value = searchHistoryRepo.getSearchHistory() + } + } + + suspend fun addRecentSearchItem(name: String, address: String, time: Long) { + viewModelScope.launch { + val exitData = searchHistoryRepo.findSearchItem(name, address) + + if (exitData != null) { + searchHistoryRepo.updateTime(name, address, time) + } else { + searchHistoryRepo.insertSearchData(name, address, time) + } + } + } + + fun getRecentDataLiveData(): LiveData> { + return searchHistoryDataList + } + + suspend fun deleteRecentData(data: String, address: String) { + searchHistoryRepo.deleteSearchData(data, address) + _searchHistoryDataList.value = searchHistoryRepo.getSearchHistory() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewModel/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewModel/SearchViewModel.kt new file mode 100644 index 00000000..82771055 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewModel/SearchViewModel.kt @@ -0,0 +1,28 @@ +package campus.tech.kakao.map.viewModel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import campus.tech.kakao.map.repository.SearchResultRepository +import campus.tech.kakao.map.retrofit.Document +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val repository: SearchResultRepository +) : ViewModel() { + + private val _searchDataList = MutableLiveData>() + private val searchResults: LiveData> get() = _searchDataList + + fun loadResultData(searchQuery: String) { + repository.loadResultMapData(searchQuery) { documents -> + _searchDataList.postValue(documents) + } + } + + fun getSearchDataLiveData(): LiveData> { + return searchResults + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/loading_icon.xml b/app/src/main/res/drawable/loading_icon.xml new file mode 100644 index 00000000..42277f69 --- /dev/null +++ b/app/src/main/res/drawable/loading_icon.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/location_map_icon.xml b/app/src/main/res/drawable/location_map_icon.xml new file mode 100644 index 00000000..21e1b4e6 --- /dev/null +++ b/app/src/main/res/drawable/location_map_icon.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/map_label.png b/app/src/main/res/drawable/map_label.png new file mode 100644 index 0000000000000000000000000000000000000000..9f7f4b27ec693a986576b5059dc0f593597d1b9e GIT binary patch literal 8726 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 WRN`>K`S0=uKwU*gx%#P1#D4%<3Yzl( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/no_search.xml b/app/src/main/res/drawable/no_search.xml new file mode 100644 index 00000000..26b2b718 --- /dev/null +++ b/app/src/main/res/drawable/no_search.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/reload_icon.xml b/app/src/main/res/drawable/reload_icon.xml new file mode 100644 index 00000000..62ac46bc --- /dev/null +++ b/app/src/main/res/drawable/reload_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/search_icon.xml b/app/src/main/res/drawable/search_icon.xml new file mode 100644 index 00000000..ead12721 --- /dev/null +++ b/app/src/main/res/drawable/search_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_data_search.xml b/app/src/main/res/layout/activity_data_search.xml new file mode 100644 index 00000000..6bc6b12b --- /dev/null +++ b/app/src/main/res/layout/activity_data_search.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_home_map.xml b/app/src/main/res/layout/activity_home_map.xml new file mode 100644 index 00000000..2ae27152 --- /dev/null +++ b/app/src/main/res/layout/activity_home_map.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + \ 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 deleted file mode 100644 index 24d17df2..00000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/activity_map_error.xml b/app/src/main/res/layout/activity_map_error.xml new file mode 100644 index 00000000..4e6f5303 --- /dev/null +++ b/app/src/main/res/layout/activity_map_error.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/map_detail_bottom_sheet.xml b/app/src/main/res/layout/map_detail_bottom_sheet.xml new file mode 100644 index 00000000..da104982 --- /dev/null +++ b/app/src/main/res/layout/map_detail_bottom_sheet.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recent_search_item.xml b/app/src/main/res/layout/recent_search_item.xml new file mode 100644 index 00000000..c8430949 --- /dev/null +++ b/app/src/main/res/layout/recent_search_item.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/search_data_item.xml b/app/src/main/res/layout/search_data_item.xml new file mode 100644 index 00000000..8dd3c3dd --- /dev/null +++ b/app/src/main/res/layout/search_data_item.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/mycolors.xml b/app/src/main/res/values/mycolors.xml new file mode 100644 index 00000000..42f10415 --- /dev/null +++ b/app/src/main/res/values/mycolors.xml @@ -0,0 +1,5 @@ + + + #00ff0000 + #d8d8d8 + \ No newline at end of file diff --git a/app/src/main/res/values/mystrings.xml b/app/src/main/res/values/mystrings.xml new file mode 100644 index 00000000..421cbb0c --- /dev/null +++ b/app/src/main/res/values/mystrings.xml @@ -0,0 +1,10 @@ + + + 검색어를 입력해 주세요. + 검색 결과가 없습니다. + 누르면 입력중인 검색어가 사라지는 버튼입니다. + 지도의 위치 모양 이미지입니다. + 누르면 최근 검색어가 삭제되는 버튼입니다. + 검색어를 입력해 주세요. + 지도 인증을 실패했습니다.\n다시 시도해 주세요. + \ No newline at end of file diff --git a/app/src/test/java/campus/tech/kakao/map/MapErrorViewModelTest.kt b/app/src/test/java/campus/tech/kakao/map/MapErrorViewModelTest.kt new file mode 100644 index 00000000..23026999 --- /dev/null +++ b/app/src/test/java/campus/tech/kakao/map/MapErrorViewModelTest.kt @@ -0,0 +1,36 @@ +package campus.tech.kakao.map + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import campus.tech.kakao.map.viewModel.MapErrorViewModel +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +class MapErrorViewModelTest { + @get: Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private lateinit var viewModel: MapErrorViewModel + + @Mock + private lateinit var errorMessageObserver: Observer + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + viewModel = MapErrorViewModel() + viewModel.errorMessage.observeForever(errorMessageObserver) + } + + @Test + fun 오류메시지가_정상적으로_설정됨() = runTest { + val error = "java.lang.Exception: 401" + viewModel.setErrorMsg(error) + Mockito.verify(errorMessageObserver).onChanged("401") + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 49f2a696..76c951ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false id("com.google.dagger.hilt.android") version "2.48.1" apply false - id("com.google.gms.google-services") version "4.4.2" apply false + //id("com.google.gms.google-services") version "4.4.2" apply false } allprojects { From a83e734265d0c7f62e4bb1f28cb52d7a84c30924 Mon Sep 17 00:00:00 2001 From: yb0x00 Date: Fri, 2 Aug 2024 00:57:00 +0900 Subject: [PATCH 2/4] docs: Add feature list to README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구현할 기능 목록 정리 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 99676874..5750a71f 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ # android-map-notification + +### 구현할 기능 목록 +- 서비스 상태를 나타내는 매개변수를 Firebase의 Remote Config에 등록 +- serviceState 값이 ON_SERVICE일 때만 초기 진입 화면이 지도 화면으로 넘어감 +- serviceState 값이 ON_SERVICE이 아닌 경우에는 serviceMessage 값을 초기 진입 화면 하단에 표시하고 지도 화면으로 진입하지 않음 \ No newline at end of file From d214561dbe7265e0b733dd40e4de8c075d37f665 Mon Sep 17 00:00:00 2001 From: yb0x00 Date: Fri, 2 Aug 2024 00:59:43 +0900 Subject: [PATCH 3/4] feat: Implement initial screen navigation to map based on Remote Config's serviceState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - serviceState 값이 ON_SERVICE일 때만 초기 진입 화면이 지도 화면으로 넘어감 --- app/build.gradle.kts | 10 ++-- app/src/main/AndroidManifest.xml | 16 +++++-- .../java/campus/tech/kakao/map/Application.kt | 4 +- .../kakao/map/data/firebase/RemoteConfig.kt | 36 ++++++++++++++ .../kakao/map/repository/SplashRepository.kt | 26 ++++++++++ .../tech/kakao/map/view/SplashActivity.kt | 48 +++++++++++++++++++ .../kakao/map/viewModel/SplashViewModel.kt | 30 ++++++++++++ .../main/res/drawable/map_navigation_icon.xml | 17 +++++++ app/src/main/res/layout/activity_splash.xml | 34 +++++++++++++ app/src/main/res/values/mycolors.xml | 1 + build.gradle.kts | 2 +- 11 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/campus/tech/kakao/map/data/firebase/RemoteConfig.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt create mode 100644 app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt create mode 100644 app/src/main/res/drawable/map_navigation_icon.xml create mode 100644 app/src/main/res/layout/activity_splash.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 51bb607d..98ceb216 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { id("kotlin-parcelize") id("kotlin-kapt") id("com.google.dagger.hilt.android") - //id("com.google.gms.google-services") + id("com.google.gms.google-services") } android { @@ -79,10 +79,10 @@ dependencies { kapt("com.google.dagger:hilt-compiler:2.48.1") implementation("androidx.activity:activity-ktx:1.9.0") implementation("androidx.room:room-ktx:2.6.1") - //implementation(platform("com.google.firebase:firebase-bom:33.1.2")) - //implementation("com.google.firebase:firebase-analytics-ktx") - //implementation("com.google.firebase:firebase-config-ktx:22.0.0") - //implementation("com.google.firebase:firebase-messaging-ktx:24.0.0") + implementation(platform("com.google.firebase:firebase-bom:33.1.2")) + implementation("com.google.firebase:firebase-analytics-ktx") + implementation("com.google.firebase:firebase-config-ktx:22.0.0") + implementation("com.google.firebase:firebase-messaging-ktx:24.0.0") testImplementation("androidx.room:room-testing:2.6.1") testImplementation("junit:junit:4.13.2") testImplementation("io.mockk:mockk-android:1.13.11") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1f358101..3e8fb86f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + - @@ -28,10 +26,18 @@ + + + diff --git a/app/src/main/java/campus/tech/kakao/map/Application.kt b/app/src/main/java/campus/tech/kakao/map/Application.kt index ea681f61..767d1779 100644 --- a/app/src/main/java/campus/tech/kakao/map/Application.kt +++ b/app/src/main/java/campus/tech/kakao/map/Application.kt @@ -1,6 +1,7 @@ package campus.tech.kakao.map import android.app.Application +import com.google.firebase.FirebaseApp import com.kakao.vectormap.KakaoMapSdk import dagger.hilt.android.HiltAndroidApp @@ -8,9 +9,10 @@ import dagger.hilt.android.HiltAndroidApp class Application : Application() { override fun onCreate(){ super.onCreate() - //카카오맵 SDK 초기화 val key = getString(R.string.kakao_api_key) KakaoMapSdk.init(this, key) + + FirebaseApp.initializeApp(this) } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/firebase/RemoteConfig.kt b/app/src/main/java/campus/tech/kakao/map/data/firebase/RemoteConfig.kt new file mode 100644 index 00000000..6a5c6e7b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/firebase/RemoteConfig.kt @@ -0,0 +1,36 @@ +package campus.tech.kakao.map.data.firebase + +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfigSettings +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RemoteConfig @Inject constructor() { + + private val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig + + init { + val configSettings = remoteConfigSettings { + minimumFetchIntervalInSeconds = 0 + } + remoteConfig.setConfigSettingsAsync(configSettings) + } + + fun fetchAndActivate(onComplete: (Boolean) -> Unit) { + remoteConfig.fetchAndActivate() + .addOnCompleteListener { task -> + if (task.isSuccessful) { + onComplete(true) + } else { + onComplete(false) + } + } + } + + fun getData(key: String): String { + return remoteConfig.getString(key) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt new file mode 100644 index 00000000..5043d15a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt @@ -0,0 +1,26 @@ +package campus.tech.kakao.map.repository + +import android.util.Log +import campus.tech.kakao.map.data.firebase.RemoteConfig +import javax.inject.Inject + +class SplashRepository @Inject constructor( + private val remoteConfig: RemoteConfig +) { + fun getServiceState(onComplete: (String?) -> Unit) { + remoteConfig.fetchAndActivate { isActivated -> + if (isActivated) { + val state = remoteConfig.getData(SERVICE_STATE_KEY) + Log.d("yeong", "Repository: state = $state") + onComplete(state) + } else { + Log.d("yeong", "Repository: 호출 오류") + onComplete(null) + } + } + } + + companion object { + const val SERVICE_STATE_KEY = "serviceState" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt new file mode 100644 index 00000000..d9c5b331 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt @@ -0,0 +1,48 @@ +package campus.tech.kakao.map.view + +import android.content.Intent +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivitySplashBinding +import campus.tech.kakao.map.viewModel.SplashViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SplashActivity : AppCompatActivity() { + private lateinit var binding: ActivitySplashBinding + private val splashViewModel: SplashViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setBinding() + splashViewModel.fetchServiceState() + setScreen() + + } + + private fun setBinding() { + binding = DataBindingUtil.setContentView(this, R.layout.activity_splash) + } + + private fun setScreen() { + splashViewModel.navigateToHome.observe(this, Observer { navigateToHome -> + if (navigateToHome) { + navigateToHomeMapActivity() + } else { + + } + }) + } + + private fun navigateToHomeMapActivity() { + val intent = Intent(this, HomeMapActivity::class.java) + startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt new file mode 100644 index 00000000..95c191ff --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt @@ -0,0 +1,30 @@ +package campus.tech.kakao.map.viewModel + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import campus.tech.kakao.map.repository.SplashRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SplashViewModel @Inject constructor( + private val splashRepository: SplashRepository +) : ViewModel() { + + private val _navigateToHome = MutableLiveData() + val navigateToHome: LiveData get() = _navigateToHome + + fun fetchServiceState() { + splashRepository.getServiceState { serviceState -> + if (serviceState == "ON_SERVICE") { + Log.d("yeong", "ViewModel: 통과") + _navigateToHome.value = true + } else { + Log.d("yeong", "ViewModel: 에러") + _navigateToHome.value = false + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/map_navigation_icon.xml b/app/src/main/res/drawable/map_navigation_icon.xml new file mode 100644 index 00000000..013ea900 --- /dev/null +++ b/app/src/main/res/drawable/map_navigation_icon.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 00000000..b4f46ad2 --- /dev/null +++ b/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/mycolors.xml b/app/src/main/res/values/mycolors.xml index 42f10415..1a6040a2 100644 --- a/app/src/main/res/values/mycolors.xml +++ b/app/src/main/res/values/mycolors.xml @@ -2,4 +2,5 @@ #00ff0000 #d8d8d8 + #808080 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 76c951ae..49f2a696 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false id("com.google.dagger.hilt.android") version "2.48.1" apply false - //id("com.google.gms.google-services") version "4.4.2" apply false + id("com.google.gms.google-services") version "4.4.2" apply false } allprojects { From 73648451a52976c978030e0732ab6689e6941f03 Mon Sep 17 00:00:00 2001 From: yb0x00 Date: Fri, 2 Aug 2024 01:16:01 +0900 Subject: [PATCH 4/4] feat: Show error message on initial screen if Remote Config's serviceState is not ON_SERVICE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - serviceState 값이 ON_SERVICE이 아닌 경우에는 serviceMessage 값을 초기 진입 화면 하단에 표시하고 지도 화면으로 진입하지 않음 --- .../kakao/map/repository/SplashRepository.kt | 5 +++++ .../tech/kakao/map/view/SplashActivity.kt | 8 +++++-- .../kakao/map/viewModel/SplashViewModel.kt | 4 ++++ app/src/main/res/layout/activity_splash.xml | 22 +++++++++++++------ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt index 5043d15a..4423afc7 100644 --- a/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt @@ -20,7 +20,12 @@ class SplashRepository @Inject constructor( } } + fun getErrorMsg(): String { + return remoteConfig.getData(SERVICE_MESSAGE_KEY) + } + companion object { const val SERVICE_STATE_KEY = "serviceState" + const val SERVICE_MESSAGE_KEY = "serviceMessage" } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt index d9c5b331..11bde282 100644 --- a/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt @@ -22,6 +22,7 @@ class SplashActivity : AppCompatActivity() { enableEdgeToEdge() setBinding() + setViewModel() splashViewModel.fetchServiceState() setScreen() @@ -31,12 +32,15 @@ class SplashActivity : AppCompatActivity() { binding = DataBindingUtil.setContentView(this, R.layout.activity_splash) } + private fun setViewModel() { + binding.lifecycleOwner = this + binding.viewModel = splashViewModel + } + private fun setScreen() { splashViewModel.navigateToHome.observe(this, Observer { navigateToHome -> if (navigateToHome) { navigateToHomeMapActivity() - } else { - } }) } diff --git a/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt index 95c191ff..7494bd0f 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt @@ -16,6 +16,9 @@ class SplashViewModel @Inject constructor( private val _navigateToHome = MutableLiveData() val navigateToHome: LiveData get() = _navigateToHome + private val _errorMsg = MutableLiveData() + val errorMsg: LiveData get() = _errorMsg + fun fetchServiceState() { splashRepository.getServiceState { serviceState -> if (serviceState == "ON_SERVICE") { @@ -24,6 +27,7 @@ class SplashViewModel @Inject constructor( } else { Log.d("yeong", "ViewModel: 에러") _navigateToHome.value = false + _errorMsg.value = splashRepository.getErrorMsg() } } } diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml index b4f46ad2..5ac3623a 100644 --- a/app/src/main/res/layout/activity_splash.xml +++ b/app/src/main/res/layout/activity_splash.xml @@ -3,6 +3,13 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + + + + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" />