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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 803085bd..98ceb216 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") @@ -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") 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..3e8fb86f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ @@ -25,6 +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 new file mode 100644 index 00000000..767d1779 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/Application.kt @@ -0,0 +1,18 @@ +package campus.tech.kakao.map + +import android.app.Application +import com.google.firebase.FirebaseApp +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) + + FirebaseApp.initializeApp(this) + } +} \ 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/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/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/repository/SplashRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt new file mode 100644 index 00000000..4423afc7 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/SplashRepository.kt @@ -0,0 +1,31 @@ +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) + } + } + } + + 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/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/view/SplashActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt new file mode 100644 index 00000000..11bde282 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/SplashActivity.kt @@ -0,0 +1,52 @@ +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() + setViewModel() + splashViewModel.fetchServiceState() + setScreen() + + } + + private fun setBinding() { + 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() + } + }) + } + + 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/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/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..7494bd0f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewModel/SplashViewModel.kt @@ -0,0 +1,34 @@ +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 + + private val _errorMsg = MutableLiveData() + val errorMsg: LiveData get() = _errorMsg + + fun fetchServiceState() { + splashRepository.getServiceState { serviceState -> + if (serviceState == "ON_SERVICE") { + Log.d("yeong", "ViewModel: 통과") + _navigateToHome.value = true + } else { + Log.d("yeong", "ViewModel: 에러") + _navigateToHome.value = false + _errorMsg.value = splashRepository.getErrorMsg() + } + } + } +} \ 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 00000000..9f7f4b27 Binary files /dev/null and b/app/src/main/res/drawable/map_label.png differ 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/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/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 00000000..5ac3623a --- /dev/null +++ b/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + \ 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..1a6040a2 --- /dev/null +++ b/app/src/main/res/values/mycolors.xml @@ -0,0 +1,6 @@ + + + #00ff0000 + #d8d8d8 + #808080 + \ 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