diff --git a/README.md b/README.md index f091e3fc..9b5ef63f 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,8 @@ -# android-map-location +# android-map-refactoring ## step1 기능 목록 -1. 저장된 검색어 목록에 기능 추가하기 - - 저장된 검색어 중 하나를 선택하면 해당 검색어의 검색 결과 목록이 표시된다. -2. 검색 결과 목록에 기능 추가하기 - - 검색 결과 목록 중 하나의 항목을 선택하면 해당 항목의 위치를 지도에 표시한다. -3. 마지막 위치 저장하기 - - 앱 종료 시 마지막 위치를 SharedPreference 저장하여 다시 앱 실행 시 해당 위치로 포커스 한다. -4. 에러 처리하기 - - 카카오지도 onMapError() 호출 시 에러 화면을 보여준다. "지도 인증을 실패했습니다. 다시 시도해주세요. '에러이름(에러코드): 에러메세지' +- 데이터베이스를 Room으로 변경한다. +- Hilt를 사용하여 의존성 주입을 적용한다. ## step2 기능 목록 -- 테스트 코드 - JUnit과 mockito를 이용하여 단위 테스트 코드를 작성한다. - - data source 테스트 - - local - - SavedLocation 저장 테스트 - - SavedLocation 삭제 테스트 - - SavedLocation 조회 테스트 - - 마지막 위치 저장 테스트 - - UI 테스트 코드 - - 검색 페이지 - - 검색 결과 목록 테스트 - - 검색어 저장 목록 테스트 - - clearButton 테스트 - - 지도 페이지 - - BottomSheet 테스트 - - KakaoMap Label 테스트 - - 지도 에러 화면 테스트 \ No newline at end of file +- DataBinding을 사용한다. +- LiveData를 사용한다. +- 비동기 처리를 Coroutine으로 변경한다. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 90173142..6caa059d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,9 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") + id("kotlin-parcelize") } android { @@ -10,6 +13,8 @@ android { compileSdk = 34 buildFeatures { buildConfig = true + dataBinding = true + viewBinding = true } defaultConfig { applicationId = "campus.tech.kakao.map" @@ -79,4 +84,11 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:2.11.0") implementation("com.squareup.retrofit2:converter-gson:2.11.0") androidTestImplementation("androidx.test.espresso:espresso-intents:3.3.0") + // room + implementation("androidx.room:room-runtime:2.6.1") + kapt("androidx.room:room-compiler:2.6.1") + implementation("androidx.room:room-ktx:2.4.3") + // hilt + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") } diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MainActivityUiTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MainActivityUiTest.kt index ee6a22f0..4267d204 100644 --- a/app/src/androidTest/java/campus/tech/kakao/map/MainActivityUiTest.kt +++ b/app/src/androidTest/java/campus/tech/kakao/map/MainActivityUiTest.kt @@ -4,29 +4,22 @@ import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.replaceText -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import campus.tech.kakao.map.model.Location import campus.tech.kakao.map.view.map.MapActivity -import campus.tech.kakao.map.view.search.LocationAdapter import campus.tech.kakao.map.view.search.MainActivity -import org.hamcrest.Matchers.allOf -import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import kotlin.random.Random @RunWith(AndroidJUnit4::class) class MainActivityUiTest { @@ -35,9 +28,9 @@ class MainActivityUiTest { @Test fun `검색창에_검색어를_입력하고_clearButton을_클릭하면_입력한_검색어가_삭제된다`() { - onView(withId(R.id.SearchEditTextInMain)).perform(replaceText("카페")).check(matches(withText("카페"))) + onView(withId(R.id.searchEditTextMain)).perform(replaceText("카페")).check(matches(withText("카페"))) onView(withId(R.id.clearButton)).perform(click()) - onView(withId(R.id.SearchEditTextInMain)).check(matches(withText(""))) + onView(withId(R.id.searchEditTextMain)).check(matches(withText(""))) } @Test @@ -48,14 +41,14 @@ class MainActivityUiTest { @Test fun `검색창에_부산대를_입력하고_리싸이클러뷰의_0번_아이템을_클릭하면_지도와_BottomSheet가_화면에_표시된다`() { Intents.init() - onView(withId(R.id.SearchEditTextInMain)).perform(replaceText("부산대")) + onView(withId(R.id.searchEditTextMain)).perform(replaceText("부산대")) Thread.sleep(3000) onView(withId(R.id.locationRecyclerView)) .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())) intended(hasExtra("title", "부산대학교 부산캠퍼스")) intended(hasComponent(MapActivity::class.java.name)) - onView(withId(R.id.bottom_sheet_layout)).check(matches(isDisplayed())) + onView(withId(R.id.bottomSheetLayout)).check(matches(isDisplayed())) onView(withId(R.id.map_view)).check(matches(isDisplayed())) } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/App.kt b/app/src/main/java/campus/tech/kakao/map/App.kt index 5f688ad8..81ca450b 100644 --- a/app/src/main/java/campus/tech/kakao/map/App.kt +++ b/app/src/main/java/campus/tech/kakao/map/App.kt @@ -3,15 +3,12 @@ package campus.tech.kakao.map import android.app.Application import campus.tech.kakao.map.model.datasource.SharedPreferences import com.kakao.vectormap.KakaoMapSdk +import dagger.hilt.android.HiltAndroidApp +@HiltAndroidApp class App : Application(){ - companion object{ - lateinit var sharedPreferencesManager : SharedPreferences - } override fun onCreate() { - sharedPreferencesManager = SharedPreferences(applicationContext) KakaoMapSdk.init(this, BuildConfig.KAKAO_API_KEY) super.onCreate() - } } diff --git a/app/src/main/java/campus/tech/kakao/map/LocationModule.kt b/app/src/main/java/campus/tech/kakao/map/LocationModule.kt new file mode 100644 index 00000000..aa61a8e5 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/LocationModule.kt @@ -0,0 +1,44 @@ +package campus.tech.kakao.map + +import android.content.Context +import campus.tech.kakao.map.model.datasource.KakaoAPI +import campus.tech.kakao.map.model.datasource.LastLocationSharedPreferences +import campus.tech.kakao.map.model.datasource.LocationRemoteDataSource +import campus.tech.kakao.map.model.datasource.SharedPreferences +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocationModule { + + @Singleton + @Provides + fun provideLocationRemoteDataSource(kakaoAPI: KakaoAPI): LocationRemoteDataSource { + return LocationRemoteDataSource(kakaoAPI, Dispatchers.IO) + } + + @Singleton + @Provides + fun provideDispatchers(): CoroutineDispatcher { + return Dispatchers.IO + } + + @Provides + @Singleton + fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + return SharedPreferences(context) + } + + @Provides + @Singleton + fun provideLastLocationSharedPreferences(sharedPreferences: SharedPreferences): LastLocationSharedPreferences { + return LastLocationSharedPreferences(sharedPreferences) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/RepositoryModule.kt b/app/src/main/java/campus/tech/kakao/map/RepositoryModule.kt new file mode 100644 index 00000000..bdbc3e66 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/RepositoryModule.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map + +import campus.tech.kakao.map.model.repository.DefaultLocationRepository +import campus.tech.kakao.map.model.repository.DefaultSavedLocationRepository +import campus.tech.kakao.map.model.repository.LocationRepository +import campus.tech.kakao.map.model.repository.SavedLocationRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + @Singleton + abstract fun bindDefaultLocationRepository(impl: DefaultLocationRepository) : LocationRepository + + @Binds + @Singleton + abstract fun bindDefaultSavedLocationRepository(impl: DefaultSavedLocationRepository) : SavedLocationRepository +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/repository/RetrofitInstance.kt b/app/src/main/java/campus/tech/kakao/map/RetrofitInstanceModule.kt similarity index 52% rename from app/src/main/java/campus/tech/kakao/map/model/repository/RetrofitInstance.kt rename to app/src/main/java/campus/tech/kakao/map/RetrofitInstanceModule.kt index f75e11b2..43ce45ce 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/repository/RetrofitInstance.kt +++ b/app/src/main/java/campus/tech/kakao/map/RetrofitInstanceModule.kt @@ -1,19 +1,31 @@ -package campus.tech.kakao.map.model.repository +package campus.tech.kakao.map import android.os.Looper -import campus.tech.kakao.map.BuildConfig +import android.widget.Toast +import campus.tech.kakao.map.model.datasource.KakaoAPI +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton -object RetrofitInstance { - val retrofitClient = Retrofit.Builder() - .baseUrl(BuildConfig.BASE_URL) - .addConverterFactory(GsonConverterFactory.create()) - .client(getOkHttpClient()) - .build() +@Module +@InstallIn(SingletonComponent::class) +object RetrofitInstanceModule { + @Provides + @Singleton + fun provideRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(getOkHttpClient()) + .build() + } private fun getOkHttpClient(): OkHttpClient { val client = OkHttpClient.Builder() @@ -28,13 +40,7 @@ object RetrofitInstance { if (response.code != 200) { android.os.Handler(Looper.getMainLooper()).post { - // 백그라운드 스레드에서 작업 중 UI 스레드에서 실행하도록 변경 - // Toast 메세지를 띄우게 하고 싶은데 context 자리에 뭘 넣어야할지 모르겠습니다!! -// Toast.makeText( -// , -// "Error: ${response.code} - ${response.message}", -// Toast.LENGTH_SHORT -// ).show() + Toast.makeText(App(), "Error: ${response.code} - ${response.message}", Toast.LENGTH_SHORT).show() } } return response @@ -42,8 +48,9 @@ object RetrofitInstance { } return client.addInterceptor(interceptor).build() } - - fun getInstance(): Retrofit{ - return retrofitClient + @Provides + @Singleton + fun provideKakaoAPI(retrofit: Retrofit): KakaoAPI { + return retrofit.create(KakaoAPI::class.java) } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/SavedLocationModule.kt b/app/src/main/java/campus/tech/kakao/map/SavedLocationModule.kt new file mode 100644 index 00000000..97891b6f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/SavedLocationModule.kt @@ -0,0 +1,32 @@ +package campus.tech.kakao.map + +import android.content.Context +import androidx.room.Room +import campus.tech.kakao.map.model.datasource.SavedLocationDao +import campus.tech.kakao.map.model.datasource.SavedLocationDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SavedLocationModule { + @Singleton + @Provides + fun provideSavedLocationDatabase(@ApplicationContext context: Context): SavedLocationDatabase { + return Room.databaseBuilder( + context, + SavedLocationDatabase::class.java, + SavedLocationDatabase.DATABASE_NAME + ).build() + } + + @Singleton + @Provides + fun provideSavedLocationDao(savedLocationDatabase: SavedLocationDatabase): SavedLocationDao { + return savedLocationDatabase.savedLocationDao() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/Contract.kt b/app/src/main/java/campus/tech/kakao/map/model/Contract.kt deleted file mode 100644 index ffe4a09b..00000000 --- a/app/src/main/java/campus/tech/kakao/map/model/Contract.kt +++ /dev/null @@ -1,17 +0,0 @@ -package campus.tech.kakao.map.model - -import android.provider.BaseColumns - -object Contract { - object LocationEntry: BaseColumns{ - const val TABLE_NAME = "locations" - const val COLUMN_NAME_TITLE = "name" - const val COLUMN_NAME_ADDRESS = "address" - const val COLUMN_NAME_CATEGORY = "category" - } - - object SavedLocationEntry: BaseColumns{ - const val TABLE_NAME = "saved_locations" - const val COLUMN_NAME_TITLE = "name" - } -} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/Location.kt b/app/src/main/java/campus/tech/kakao/map/model/Location.kt index a43c7125..a9bdd38f 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/Location.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/Location.kt @@ -1,15 +1,20 @@ package campus.tech.kakao.map.model +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import java.io.Serializable +@Parcelize data class Location( + val id: Long, val title: String, val address: String, val category: String, val longitude: Double, val latitude: Double -){ +) : Parcelable { companion object { fun LocationDto.toLocation(): Location { - return Location(title, address, category, x.toDouble(), y.toDouble()) + return Location(id.toLong(), title, address, category, x.toDouble(), y.toDouble()) } } diff --git a/app/src/main/java/campus/tech/kakao/map/model/LocationDbHelper.kt b/app/src/main/java/campus/tech/kakao/map/model/LocationDbHelper.kt deleted file mode 100644 index b74b29ac..00000000 --- a/app/src/main/java/campus/tech/kakao/map/model/LocationDbHelper.kt +++ /dev/null @@ -1,40 +0,0 @@ -package campus.tech.kakao.map.model - -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import campus.tech.kakao.map.model.Contract.LocationEntry -import campus.tech.kakao.map.model.Contract.SavedLocationEntry -class LocationDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { - - private val SQL_CREATE_LOCATION_TABLE = - "CREATE TABLE ${LocationEntry.TABLE_NAME} (" + - "${LocationEntry.COLUMN_NAME_TITLE} TEXT primary key," + - "${LocationEntry.COLUMN_NAME_ADDRESS} TEXT,"+ - "${LocationEntry.COLUMN_NAME_CATEGORY} TEXT"+ - ")" - - private val SQL_DELETE_LOCATION_TABLE = "DROP TABLE IF EXISTS ${LocationEntry.TABLE_NAME}" - - private val SQL_CREATE_SAVED_LOCATION_TABLE = - "CREATE TABLE ${SavedLocationEntry.TABLE_NAME} (" + - "${SavedLocationEntry.COLUMN_NAME_TITLE} TEXT primary key"+ - ")" - - private val SQL_DELETE_SAVED_LOCATION_TABLE = - "DROP TABLE IF EXISTS ${SavedLocationEntry.TABLE_NAME}" - - override fun onCreate(db: SQLiteDatabase) { - db.execSQL(SQL_CREATE_LOCATION_TABLE) - db.execSQL(SQL_CREATE_SAVED_LOCATION_TABLE) - } - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - db.execSQL(SQL_DELETE_LOCATION_TABLE) - db.execSQL(SQL_DELETE_SAVED_LOCATION_TABLE) - onCreate(db) - } - companion object { - const val DATABASE_VERSION = 1 - const val DATABASE_NAME = "map.db" - } -} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/SavedLocation.kt b/app/src/main/java/campus/tech/kakao/map/model/SavedLocation.kt index d09e0cb2..32ab8e81 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/SavedLocation.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/SavedLocation.kt @@ -1,5 +1,11 @@ package campus.tech.kakao.map.model +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "savedLocations") data class SavedLocation( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, val title: String ) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/repository/KakaoAPI.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/KakaoAPI.kt similarity index 89% rename from app/src/main/java/campus/tech/kakao/map/model/repository/KakaoAPI.kt rename to app/src/main/java/campus/tech/kakao/map/model/datasource/KakaoAPI.kt index 6e2b2dac..05d7d10f 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/repository/KakaoAPI.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/KakaoAPI.kt @@ -1,4 +1,4 @@ -package campus.tech.kakao.map.model.repository +package campus.tech.kakao.map.model.datasource import campus.tech.kakao.map.model.SearchFromKeywordResponse import retrofit2.Response diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/LastLocationSharedPreferences.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/LastLocationSharedPreferences.kt new file mode 100644 index 00000000..c0f7851a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/LastLocationSharedPreferences.kt @@ -0,0 +1,28 @@ +package campus.tech.kakao.map.model.datasource + +import campus.tech.kakao.map.model.Location +import javax.inject.Inject + +class LastLocationSharedPreferences @Inject constructor( + private val sharedPreferences: SharedPreferences +) { + fun putLastLocation(location: Location) { + sharedPreferences.putString("id", location.id.toString()) + sharedPreferences.putString("longitude", location.longitude.toString()) + sharedPreferences.putString("latitude", location.latitude.toString()) + sharedPreferences.putString("title", location.title.toString()) + sharedPreferences.putString("address", location.address.toString()) + sharedPreferences.putString("category", location.category.toString()) + } + + fun getLastLocation(): Location? { + val id = sharedPreferences.getString("id", "").toString() + if(id == "") return null + val title = sharedPreferences.getString("title", "") + val longitude = sharedPreferences.getString("longitude", "").toString().toDouble() + val latitude = sharedPreferences.getString("latitude", "").toString().toDouble() + val address = sharedPreferences.getString("address", "").toString() + val category = sharedPreferences.getString("category", "").toString() + return Location(id.toLong(), title, address, category, longitude, latitude) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/LastLocationlDataSource.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/LastLocationlDataSource.kt deleted file mode 100644 index e2013e90..00000000 --- a/app/src/main/java/campus/tech/kakao/map/model/datasource/LastLocationlDataSource.kt +++ /dev/null @@ -1,27 +0,0 @@ -package campus.tech.kakao.map.model.datasource - -import campus.tech.kakao.map.App -import campus.tech.kakao.map.model.Location - -class LastLocationlDataSource() { - - fun putLastLocation(location: Location) { - if (location != null) { - App.sharedPreferencesManager.putString("longitude", location.longitude.toString()) - App.sharedPreferencesManager.putString("latitude", location.latitude.toString()) - App.sharedPreferencesManager.putString("title", location.title.toString()) - App.sharedPreferencesManager.putString("address", location.address.toString()) - App.sharedPreferencesManager.putString("category", location.category.toString()) - } - } - - fun getLastLocation(): Location? { - val title = App.sharedPreferencesManager.getString("title", "") - if(title == "") return null - val longitude = App.sharedPreferencesManager.getString("longitude", "").toString().toDouble() - val latitude = App.sharedPreferencesManager.getString("latitude", "").toString().toDouble() - val address = App.sharedPreferencesManager.getString("address", "").toString() - val category = App.sharedPreferencesManager.getString("category", "").toString() - return Location(title, address, category, longitude, latitude) - } -} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationDataSource.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationDataSource.kt deleted file mode 100644 index c2228444..00000000 --- a/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationDataSource.kt +++ /dev/null @@ -1,32 +0,0 @@ -package campus.tech.kakao.map.model.datasource - -import android.util.Log -import campus.tech.kakao.map.model.Location -import campus.tech.kakao.map.model.Location.Companion.toLocation -import campus.tech.kakao.map.model.LocationDto -import campus.tech.kakao.map.model.repository.KakaoAPI -import campus.tech.kakao.map.model.repository.RetrofitInstance -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class LocationDataSource { - companion object{ - private const val RESULT_SIZE = 15 - } - - private val client = RetrofitInstance.getInstance().create(KakaoAPI::class.java) - - suspend fun getLocations(keyword: String): List { - return withContext(Dispatchers.IO){ - val response = client.searchFromKeyword(keyword, RESULT_SIZE) - val locationDtos: List = response.body()?.documents ?: emptyList() - Log.d("jieun", "locationDtos: " + locationDtos) - toLocations(locationDtos) - } - } - - private fun toLocations(locationDtos: List) = - locationDtos.map { - it.toLocation() - } -} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationRemoteDataSource.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationRemoteDataSource.kt new file mode 100644 index 00000000..f64bc1ad --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationRemoteDataSource.kt @@ -0,0 +1,22 @@ +package campus.tech.kakao.map.model.datasource + +import campus.tech.kakao.map.model.SearchFromKeywordResponse +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class LocationRemoteDataSource @Inject constructor( + private val kakaoAPI: KakaoAPI, + private val dispatchersIO: CoroutineDispatcher +) { + companion object{ + private const val RESULT_SIZE = 15 + } + + suspend fun getLocations(keyword: String): SearchFromKeywordResponse? { + return withContext(dispatchersIO){ + kakaoAPI.searchFromKeyword(keyword, RESULT_SIZE).body() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDao.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDao.kt new file mode 100644 index 00000000..c0428511 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDao.kt @@ -0,0 +1,19 @@ +package campus.tech.kakao.map.model.datasource + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import campus.tech.kakao.map.model.SavedLocation + +@Dao +interface SavedLocationDao { + @Insert + suspend fun insert(savedLocation: SavedLocation): Long + + @Query("SELECT * FROM savedLocations") + suspend fun getAll(): List + + @Delete + suspend fun delete(savedLocation: SavedLocation) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDataSource.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDataSource.kt deleted file mode 100644 index 9310c1e8..00000000 --- a/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDataSource.kt +++ /dev/null @@ -1,56 +0,0 @@ -package campus.tech.kakao.map.model.datasource - -import android.content.ContentValues -import android.util.Log -import campus.tech.kakao.map.model.Contract.SavedLocationEntry -import campus.tech.kakao.map.model.LocationDbHelper -import campus.tech.kakao.map.model.SavedLocation - -class SavedLocationDataSource(private val dbHelper : LocationDbHelper) { - - fun addSavedLocation(title: String): Long { - val db = dbHelper.writableDatabase - val values = ContentValues().apply { - put(SavedLocationEntry.COLUMN_NAME_TITLE, title) - } - Log.d("jieun", "insertSavedLocation 저장완료") - return db.insert(SavedLocationEntry.TABLE_NAME, null, values) - } - - fun getSavedLocationAll(): MutableList { - val db = dbHelper.readableDatabase - - val projection = arrayOf( - SavedLocationEntry.COLUMN_NAME_TITLE - ) - val sortOrder = "${SavedLocationEntry.COLUMN_NAME_TITLE} ASC" - val cursor = db.query( - SavedLocationEntry.TABLE_NAME, - projection, - null, - null, - null, - null, - sortOrder - ) - - val results = mutableListOf() - with(cursor) { - while (moveToNext()) { - val title = getString(getColumnIndexOrThrow(SavedLocationEntry.COLUMN_NAME_TITLE)) - results.add(SavedLocation(title)) - } - } - cursor.close() - return results - } - - fun deleteSavedLocation(title: String): Int { - val db = dbHelper.writableDatabase - - val selection = "${SavedLocationEntry.COLUMN_NAME_TITLE} = ?" - val selectionArgs = arrayOf(title) - - return db.delete(SavedLocationEntry.TABLE_NAME, selection, selectionArgs) - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDatabase.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDatabase.kt new file mode 100644 index 00000000..a3257d6e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDatabase.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map.model.datasource + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import campus.tech.kakao.map.model.SavedLocation + +@Database(entities = [SavedLocation::class], version = 1) +abstract class SavedLocationDatabase: RoomDatabase() { + abstract fun savedLocationDao(): SavedLocationDao + + companion object { + val DATABASE_NAME = "savedLocationDatabase" + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/SharedPreferences.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/SharedPreferences.kt index 8ccd7f09..e2eb9f04 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/datasource/SharedPreferences.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/SharedPreferences.kt @@ -1,8 +1,9 @@ package campus.tech.kakao.map.model.datasource import android.content.Context +import javax.inject.Inject -class SharedPreferences(context: Context) { +class SharedPreferences (context: Context) { private val prefs = context.getSharedPreferences("myPref", Context.MODE_PRIVATE) fun getString(key: String, defValue: String): String { diff --git a/app/src/main/java/campus/tech/kakao/map/model/repository/DefaultLocationRepository.kt b/app/src/main/java/campus/tech/kakao/map/model/repository/DefaultLocationRepository.kt new file mode 100644 index 00000000..3a09248e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/repository/DefaultLocationRepository.kt @@ -0,0 +1,33 @@ +package campus.tech.kakao.map.model.repository + +import campus.tech.kakao.map.model.Location +import campus.tech.kakao.map.model.Location.Companion.toLocation +import campus.tech.kakao.map.model.LocationDto +import campus.tech.kakao.map.model.datasource.LastLocationSharedPreferences +import campus.tech.kakao.map.model.datasource.LocationRemoteDataSource +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultLocationRepository @Inject constructor( + private val locationRemoteDataSource: LocationRemoteDataSource, + private val lastLocationSharedPreferences: LastLocationSharedPreferences +): LocationRepository { + override suspend fun getLocationAll(query: String): List { + val searchFromKeywordResponse = locationRemoteDataSource.getLocations(query) + val locationDtos: List = searchFromKeywordResponse?.documents ?: emptyList() + return toLocations(locationDtos) + } + + private fun toLocations(locationDtos: List) = + locationDtos.map { + it.toLocation() + } + override fun addLastLocation(location: Location){ + lastLocationSharedPreferences.putLastLocation(location) + } + + override fun getLastLocation(): Location? { + return lastLocationSharedPreferences.getLastLocation() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/repository/DefaultSavedLocationRepository.kt b/app/src/main/java/campus/tech/kakao/map/model/repository/DefaultSavedLocationRepository.kt new file mode 100644 index 00000000..7319cb1f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/repository/DefaultSavedLocationRepository.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.model.repository + +import campus.tech.kakao.map.model.SavedLocation +import campus.tech.kakao.map.model.datasource.SavedLocationDao +import campus.tech.kakao.map.model.datasource.SavedLocationDatabase +import javax.inject.Inject + +class DefaultSavedLocationRepository @Inject constructor( + private val savedLocationDao: SavedLocationDao +): SavedLocationRepository { + override suspend fun getSavedLocationAll(): MutableList { + val results = savedLocationDao.getAll() + return if(results.isNotEmpty()) results.toMutableList() else mutableListOf() + } + + override suspend fun addSavedLocation(savedLocation: SavedLocation) { + savedLocationDao.insert(savedLocation) + } + + override suspend fun deleteSavedLocation(savedLocation: SavedLocation) { + savedLocationDao.delete(savedLocation) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/repository/LastLocationRepository.kt b/app/src/main/java/campus/tech/kakao/map/model/repository/LastLocationRepository.kt deleted file mode 100644 index 01024b00..00000000 --- a/app/src/main/java/campus/tech/kakao/map/model/repository/LastLocationRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package campus.tech.kakao.map.model.repository - -import campus.tech.kakao.map.model.Location -import campus.tech.kakao.map.model.datasource.LastLocationlDataSource - -class LastLocationRepository( - private val locationLocalDataSource: LastLocationlDataSource -) { - fun putLastLocation(location: Location){ - locationLocalDataSource.putLastLocation(location) - } - - fun getLastLocation(): Location? { - return locationLocalDataSource.getLastLocation() - } -} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/repository/LocationRepository.kt b/app/src/main/java/campus/tech/kakao/map/model/repository/LocationRepository.kt index b8846592..9f91916e 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/repository/LocationRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/repository/LocationRepository.kt @@ -1,12 +1,9 @@ package campus.tech.kakao.map.model.repository import campus.tech.kakao.map.model.Location -import campus.tech.kakao.map.model.datasource.LocationDataSource -class LocationRepository( - private val locationRemoteDataSource: LocationDataSource -) { - suspend fun getLocationRemote(query: String): List { - return locationRemoteDataSource.getLocations(query) - } +interface LocationRepository { + suspend fun getLocationAll(query: String): List + fun addLastLocation(location: Location) + fun getLastLocation(): Location? } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/repository/SavedLocationRepository.kt b/app/src/main/java/campus/tech/kakao/map/model/repository/SavedLocationRepository.kt index e4996d97..8f5203f3 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/repository/SavedLocationRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/repository/SavedLocationRepository.kt @@ -1,25 +1,9 @@ package campus.tech.kakao.map.model.repository import campus.tech.kakao.map.model.SavedLocation -import campus.tech.kakao.map.model.datasource.SavedLocationDataSource - -class SavedLocationRepository( - private val locationLocalRepository: SavedLocationDataSource -) { - fun getSavedLocationAll(): MutableList { - val results = locationLocalRepository.getSavedLocationAll() - return if(results.isNotEmpty()) results else mutableListOf() - } - - fun addSavedLocation(title: String) { - locationLocalRepository.addSavedLocation(title) - } - - fun deleteSavedLocation(title: String): Boolean { - if (locationLocalRepository.deleteSavedLocation(title) == 1) { - return true - } - return false - } +interface SavedLocationRepository { + suspend fun getSavedLocationAll(): MutableList + suspend fun addSavedLocation(savedLocation: SavedLocation) + suspend fun deleteSavedLocation(savedLocation: SavedLocation) } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/map/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/map/MapActivity.kt index c1f66493..011ec8e2 100644 --- a/app/src/main/java/campus/tech/kakao/map/view/map/MapActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/view/map/MapActivity.kt @@ -7,13 +7,16 @@ import android.util.Log import android.view.View import android.widget.EditText import android.widget.TextView +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivityMapBinding +import campus.tech.kakao.map.databinding.ErrorMapBinding +import campus.tech.kakao.map.databinding.MapBottomSheetBinding import campus.tech.kakao.map.model.Location -import campus.tech.kakao.map.model.datasource.LastLocationlDataSource -import campus.tech.kakao.map.model.repository.LastLocationRepository import campus.tech.kakao.map.view.search.MainActivity +import campus.tech.kakao.map.viewmodel.LocationViewModel import com.google.android.material.bottomsheet.BottomSheetBehavior import com.kakao.vectormap.KakaoMap import com.kakao.vectormap.KakaoMapReadyCallback @@ -23,28 +26,30 @@ import com.kakao.vectormap.MapView import com.kakao.vectormap.label.LabelOptions import com.kakao.vectormap.label.LabelStyle import com.kakao.vectormap.label.LabelStyles +import dagger.hilt.android.AndroidEntryPoint - +@AndroidEntryPoint class MapActivity : AppCompatActivity() { - private val searchEditText by lazy { findViewById(R.id.SearchEditTextInMap) } - private val mapView by lazy { findViewById(R.id.map_view) } - private val bottomSheetLayout by lazy { findViewById(R.id.bottom_sheet_layout) } - private val bottom_sheet_title by lazy { findViewById(R.id.bottom_sheet_title) } - private val bottom_sheet_address by lazy { findViewById(R.id.bottom_sheet_address) } - private val errorMessageTextView by lazy { findViewById(R.id.errorMessageTextView) } - private val bottomSheetBehavior: BottomSheetBehavior by lazy { BottomSheetBehavior.from(bottomSheetLayout) } + private val locationViewModel: LocationViewModel by viewModels() + private lateinit var bottomSheetBehavior: BottomSheetBehavior - private val lastLocationLocalDataSource: LastLocationlDataSource by lazy { LastLocationlDataSource() } - private val lastLocationRepository: LastLocationRepository by lazy { LastLocationRepository(lastLocationLocalDataSource) } + private lateinit var activityMapBinding: ActivityMapBinding + private lateinit var errorMapBinding: ErrorMapBinding + private lateinit var mapBottomSheetBinding: MapBottomSheetBinding companion object{ - private val DEFAULT_LONGITUDE = 127.115587 - private val DEFAULT_LATITUDE = 37.406960 + private const val DEFAULT_LONGITUDE = 127.115587 + private const val DEFAULT_LATITUDE = 37.406960 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_map) + activityMapBinding = ActivityMapBinding.inflate(layoutInflater) + setContentView(activityMapBinding.root) + + errorMapBinding = ErrorMapBinding.inflate(layoutInflater) + mapBottomSheetBinding = activityMapBinding.mapBottomSheet + bottomSheetBehavior = BottomSheetBehavior.from(mapBottomSheetBinding.bottomSheetLayout) setupEditText() setupMapView() @@ -52,47 +57,45 @@ class MapActivity : AppCompatActivity() { override fun onResume() { super.onResume() - mapView.resume() // MapView 의 resume 호출 + activityMapBinding.mapView.resume() // MapView 의 resume 호출 } override fun onPause() { super.onPause() - mapView.pause() // MapView 의 pause 호출 + activityMapBinding.mapView.pause() // MapView 의 pause 호출 } private fun setupEditText() { - searchEditText.setOnClickListener { + activityMapBinding.searchEditTextInMap.setOnClickListener { val intent: Intent = Intent(this@MapActivity, MainActivity::class.java) startActivity(intent) } } private fun setupMapView() { - mapView.start(object : MapLifeCycleCallback() { + activityMapBinding.mapView.start(object : MapLifeCycleCallback() { override fun onMapDestroy() { Log.d("jieun", "onMapDestroy") } override fun onMapError(error: Exception) { // 인증 실패 및 지도 사용 중 에러가 발생할 때 호출됨 - Log.d("jieun", "onMapError" + error) + Log.d("jieun", "onMapError$error") showErrorMessage(error) } }, object : KakaoMapReadyCallback() { - val location = getCoordinates() + val location = getLocation() override fun onMapReady(kakaoMap: KakaoMap) { // 인증 후 API 가 정상적으로 실행될 때 호출됨 - Log.d("jieun", "onMapReady coordinates: " + location.toString()) + Log.d("jieun", "onMapReady location: " + location.toString()) if (location != null) { showLabel(location, kakaoMap) showBottomSheet(location) - lastLocationRepository.putLastLocation(location) -// Log.d("jieun", "onMapReady setSharedData: " + getSharedData("pref")) + locationViewModel.addLastLocation(location) } else{ hideBottomSheet() } } override fun getPosition(): LatLng { -// Log.d("jieun", "getPosition coordinates: " + coordinates.toString()) if (location != null) { return LatLng.from(location.latitude, location.longitude) } else{ @@ -106,8 +109,8 @@ class MapActivity : AppCompatActivity() { private fun showErrorMessage(error: Exception) { runOnUiThread { - setContentView(R.layout.error_map) - errorMessageTextView.text = "지도 인증을 실패했습니다.\n다시 시도해주세요.\n\n" + error.message + setContentView(errorMapBinding.root) + errorMapBinding.errorMessageTextView.text = "지도 인증을 실패했습니다.\n다시 시도해주세요.\n\n" + error.message } } @@ -121,7 +124,7 @@ class MapActivity : AppCompatActivity() { .setTextStyles(32, Color.BLACK, 1, Color.GRAY).setZoomLevel(15) ) val position = LatLng.from(location.latitude, location.longitude) - kakaoMap.labelManager?.getLayer()?.addLabel( + kakaoMap.labelManager?.layer?.addLabel( LabelOptions.from(position) .setStyles(labelStyles) .setTexts(location.title) @@ -129,36 +132,32 @@ class MapActivity : AppCompatActivity() { } private fun hideBottomSheet() { - bottomSheetLayout.visibility = View.GONE + mapBottomSheetBinding.bottomSheetLayout.visibility = View.GONE } private fun showBottomSheet(location: Location) { - bottomSheetLayout.visibility = View.VISIBLE + mapBottomSheetBinding.bottomSheetLayout.visibility = View.VISIBLE + mapBottomSheetBinding.bottomSheetTitle.text = location.title + Log.d("jieun", "mapBottomSheetBinding.bottomSheetTitle.text:"+mapBottomSheetBinding.bottomSheetTitle.text) + mapBottomSheetBinding.bottomSheetAddress.text = location.address bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - bottom_sheet_title.text = location.title - bottom_sheet_address.text = location.address } - private fun getCoordinates(): Location? { - var location = getCoordinatesByIntent() + private fun getLocation(): Location? { + var location = getLocationByIntent() if(location == null) { - location = lastLocationRepository.getLastLocation() + location = locationViewModel.getLastLocation() } return location } - private fun getCoordinatesByIntent(): Location? { - if (intent.hasExtra("title") && intent.hasExtra("longitude") - && intent.hasExtra("latitude") && intent.hasExtra("address")) { - val title = intent.getStringExtra("title") - val longitude = intent.getDoubleExtra("longitude", 0.0) - val latitude = intent.getDoubleExtra("latitude", 0.0) - val address = intent.getStringExtra("address").toString() - val category = intent.getStringExtra("category").toString() - if (title != null) { - return Location(title, address, category, longitude, latitude) - } else return null - } else return null + private fun getLocationByIntent(): Location? { + if (intent.hasExtra("location")) { + val location = intent.getParcelableExtra("location", Location::class.java) // API 레벨 오류, 실행에는 문제없다. + Log.d("jieun","getLocationByIntent location "+location.toString()) + return location + } + return null } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/ItemSelectedListener.kt b/app/src/main/java/campus/tech/kakao/map/view/search/ItemSelectedListener.kt new file mode 100644 index 00000000..29130d5d --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/search/ItemSelectedListener.kt @@ -0,0 +1,10 @@ +package campus.tech.kakao.map.view.search + +import campus.tech.kakao.map.model.Location +import campus.tech.kakao.map.model.SavedLocation + +interface ItemSelectedListener { + fun onLocationItemClicked(location: Location) + fun onSavedLocationItemClearButtonClicked(item: SavedLocation) + fun onSavedLocationItemClicked(title: String) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/LocationAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/search/LocationAdapter.kt index 07d26ada..40edac7b 100644 --- a/app/src/main/java/campus/tech/kakao/map/view/search/LocationAdapter.kt +++ b/app/src/main/java/campus/tech/kakao/map/view/search/LocationAdapter.kt @@ -1,19 +1,17 @@ package campus.tech.kakao.map.view.search import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import campus.tech.kakao.map.R -import campus.tech.kakao.map.view.search.LocationAdapter.LocationHolder +import campus.tech.kakao.map.databinding.ItemLocationBinding +import campus.tech.kakao.map.view.search.LocationAdapter.LocationViewHolder import campus.tech.kakao.map.model.Location class LocationAdapter( - private val itemSelectedListener: OnItemSelectedListener -) : ListAdapter( + private val itemSelectedListener: ItemSelectedListener +) : ListAdapter( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Location, newItem: Location): Boolean { return oldItem.title == newItem.title @@ -22,31 +20,28 @@ class LocationAdapter( return oldItem == newItem } }) { - inner class LocationHolder( - itemView: View, - itemSelectedListener: OnItemSelectedListener - ) : RecyclerView.ViewHolder(itemView) { - val titleTextView: TextView by lazy { itemView.findViewById(R.id.titleTextView) } - val addressTextView: TextView by lazy { itemView.findViewById(R.id.addressTextView) } - val categoryTextView: TextView by lazy { itemView.findViewById(R.id.categoryTextView) } - - init { - itemView.setOnClickListener { - val location = getItem(bindingAdapterPosition) - itemSelectedListener.onLocationViewClicked(location) - } - } + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): LocationViewHolder { // ViewHolder 생성 + val inflater = LayoutInflater.from(parent.context) + val itemLocationBinding = ItemLocationBinding.inflate(inflater, parent, false) + return LocationViewHolder(itemLocationBinding, itemSelectedListener) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocationHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_location, parent, false) - return LocationHolder(view, itemSelectedListener) + override fun onBindViewHolder(holder: LocationViewHolder, position: Int) { // bind를 통해 데이터를 연결 + holder.bind(getItem(position)) } - override fun onBindViewHolder(holder: LocationHolder, position: Int) { - val location = getItem(position) - holder.titleTextView.text = location.title - holder.addressTextView.text = location.address - holder.categoryTextView.text = location.category + class LocationViewHolder( // + private val itemLocationBinding: ItemLocationBinding, + private val itemSelectedListener: ItemSelectedListener + ) : RecyclerView.ViewHolder( + itemLocationBinding.root + ) { + fun bind(item: Location) { // ViewHolder와 itemLocationBinding 연동 + itemLocationBinding.location = item + itemLocationBinding.onItemSelectedListener = itemSelectedListener + } } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/view/search/MainActivity.kt index f0d1bca5..8cb94c2a 100644 --- a/app/src/main/java/campus/tech/kakao/map/view/search/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/view/search/MainActivity.kt @@ -5,86 +5,55 @@ import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View -import android.widget.EditText -import android.widget.ImageView -import android.widget.TextView +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import campus.tech.kakao.map.model.datasource.SavedLocationDataSource -import campus.tech.kakao.map.model.datasource.LocationDataSource -import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivityMainBinding import campus.tech.kakao.map.model.Location import campus.tech.kakao.map.model.SavedLocation -import campus.tech.kakao.map.model.LocationDbHelper -import campus.tech.kakao.map.model.repository.LocationRepository -import campus.tech.kakao.map.model.repository.SavedLocationRepository import campus.tech.kakao.map.view.map.MapActivity -import campus.tech.kakao.map.viewmodel.ViewModelFactory.LocationViewModelFactory -import campus.tech.kakao.map.viewmodel.ViewModelFactory.SavedLocationViewModelFactory import campus.tech.kakao.map.viewmodel.LocationViewModel import campus.tech.kakao.map.viewmodel.SavedLocationViewModel +import dagger.hilt.android.AndroidEntryPoint -class MainActivity : AppCompatActivity(), OnItemSelectedListener { - private val locationViewModel: LocationViewModel by lazy { - ViewModelProvider(this, LocationViewModelFactory(locationRepository)) - .get(LocationViewModel::class.java) - } - private val locationAdapter: LocationAdapter by lazy { LocationAdapter(this) } - private val locationRecyclerView: RecyclerView by lazy { findViewById(R.id.locationRecyclerView) } - - private val savedLocationViewModel: SavedLocationViewModel by lazy { - ViewModelProvider(this, SavedLocationViewModelFactory(savedLocationRepository)) - .get(SavedLocationViewModel::class.java) - } +@AndroidEntryPoint +class MainActivity : AppCompatActivity(), ItemSelectedListener, SearchClearButtonClickListener { + private val locationViewModel: LocationViewModel by viewModels() + private val savedLocationViewModel: SavedLocationViewModel by viewModels() - private val savedLocationAdapter: SavedLocationAdapter by lazy { SavedLocationAdapter(this) } - private val savedLocationRecyclerView: RecyclerView by lazy { - findViewById(R.id.savedLocationRecyclerView) - } - - private val locationDbHelper: LocationDbHelper by lazy { LocationDbHelper(this) } - private val locationLocalDataSource: SavedLocationDataSource by lazy { SavedLocationDataSource(locationDbHelper) } - private val locationRemoteDataSource: LocationDataSource by lazy { LocationDataSource() } - private val locationRepository: LocationRepository by lazy { LocationRepository(locationRemoteDataSource) } - private val savedLocationRepository: SavedLocationRepository by lazy { SavedLocationRepository(locationLocalDataSource) } - - private val clearButton: ImageView by lazy { findViewById(R.id.clearButton) } - private val searchEditText: EditText by lazy { findViewById(R.id.SearchEditTextInMain) } - private val noResultTextView: TextView by lazy { findViewById(R.id.NoResultTextView) } + private lateinit var locationAdapter: LocationAdapter + private lateinit var savedLocationAdapter: SavedLocationAdapter + private lateinit var activityMainBinding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + activityMainBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(activityMainBinding.root) + + locationAdapter = LocationAdapter(this) + savedLocationAdapter = SavedLocationAdapter(this) setupSearchEditText() - setupClearButton() setupViewModels() setupRecyclerViews() } private fun setupSearchEditText() { - searchEditText.addTextChangedListener(object : TextWatcher { + activityMainBinding.searchEditTextMain.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 query = s.toString() + if(query.isEmpty()) return locationViewModel.searchLocationsFromKakaoAPI(query) { searchLocationsSize -> handleNoResultMessage(searchLocationsSize) } } - override fun afterTextChanged(s: Editable?) {} }) - searchEditText.requestFocus() - } - - private fun setupClearButton() { - clearButton.setOnClickListener { - searchEditText.setText("") - } + activityMainBinding.searchEditTextMain.requestFocus() + activityMainBinding.searchClickListener = this } private fun setupViewModels() { @@ -99,9 +68,9 @@ class MainActivity : AppCompatActivity(), OnItemSelectedListener { savedLocationViewModel.savedLocation.observe(this, Observer { savedLocationAdapter.submitList(it?.toList() ?: emptyList()) if (it.size > 0) { - savedLocationRecyclerView.visibility = View.VISIBLE + activityMainBinding.savedLocationRecyclerView.visibility = View.VISIBLE } else { - savedLocationRecyclerView.visibility = View.GONE + activityMainBinding.savedLocationRecyclerView.visibility = View.GONE } }) } @@ -113,47 +82,47 @@ class MainActivity : AppCompatActivity(), OnItemSelectedListener { } private fun setupRecyclerViews() { - locationRecyclerView.layoutManager = LinearLayoutManager(this) - locationRecyclerView.adapter = locationAdapter + activityMainBinding.locationRecyclerView.layoutManager = LinearLayoutManager(this) + activityMainBinding.locationRecyclerView.adapter = locationAdapter - savedLocationRecyclerView.layoutManager = + activityMainBinding.savedLocationRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - savedLocationRecyclerView.adapter = savedLocationAdapter + activityMainBinding.savedLocationRecyclerView.adapter = savedLocationAdapter } - override fun onLocationViewClicked(location: Location) { - savedLocationViewModel.addSavedLocation(location.title) + override fun onLocationItemClicked(location: Location) { + savedLocationViewModel.addSavedLocation(location.id, location.title) val intent = Intent(this@MainActivity, MapActivity::class.java) - intent.putExtra("title", location.title) - intent.putExtra("address", location.address) - intent.putExtra("category", location.category) - intent.putExtra("longitude", location.longitude) - intent.putExtra("latitude", location.latitude) + intent.putExtra("location", location) startActivity(intent) } - override fun onSavedLocationXButtonClicked(item: SavedLocation) { - savedLocationViewModel.deleteSavedLocation(item) + override fun onSavedLocationItemClearButtonClicked(savedLocation: SavedLocation) { + savedLocationViewModel.deleteSavedLocation(savedLocation) } - override fun onSavedLocationViewClicked(title: String) { + override fun onSavedLocationItemClicked(title: String) { updateEditText(title) locationViewModel.searchLocationsFromKakaoAPI(title){ searchLocationsSize -> handleNoResultMessage(searchLocationsSize) } } + override fun onSearchClearButtonClicked() { + activityMainBinding.searchEditTextMain.setText("") + } + private fun updateEditText(title: String) { - searchEditText.setText(title) - searchEditText.setSelection(searchEditText.text.length) + activityMainBinding.searchEditTextMain.setText(title) + activityMainBinding.searchEditTextMain.setSelection(activityMainBinding.searchEditTextMain.text.length) } private fun handleNoResultMessage(searchLocationsSize: Int) { if (searchLocationsSize > 0) { - noResultTextView.visibility = View.GONE + activityMainBinding.noResultTextView.visibility = View.GONE } else { - noResultTextView.visibility = View.VISIBLE + activityMainBinding.noResultTextView.visibility = View.VISIBLE } } } diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/OnItemSelectedListener.kt b/app/src/main/java/campus/tech/kakao/map/view/search/OnItemSelectedListener.kt deleted file mode 100644 index ddd4e8d3..00000000 --- a/app/src/main/java/campus/tech/kakao/map/view/search/OnItemSelectedListener.kt +++ /dev/null @@ -1,10 +0,0 @@ -package campus.tech.kakao.map.view.search - -import campus.tech.kakao.map.model.Location -import campus.tech.kakao.map.model.SavedLocation - -interface OnItemSelectedListener { - fun onLocationViewClicked(location: Location) - fun onSavedLocationXButtonClicked(item: SavedLocation) - fun onSavedLocationViewClicked(title: String) -} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/SavedLocationAdapter.kt b/app/src/main/java/campus/tech/kakao/map/view/search/SavedLocationAdapter.kt index 97335afb..da054ab6 100644 --- a/app/src/main/java/campus/tech/kakao/map/view/search/SavedLocationAdapter.kt +++ b/app/src/main/java/campus/tech/kakao/map/view/search/SavedLocationAdapter.kt @@ -1,56 +1,42 @@ package campus.tech.kakao.map.view.search import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import campus.tech.kakao.map.R -import campus.tech.kakao.map.view.search.SavedLocationAdapter.SavedLocationHolder +import campus.tech.kakao.map.databinding.ItemSavedLocationBinding +import campus.tech.kakao.map.view.search.SavedLocationAdapter.SavedLocationViewHolder import campus.tech.kakao.map.model.SavedLocation class SavedLocationAdapter( - private val itemSelectedListener: OnItemSelectedListener -) : ListAdapter( + private val itemSelectedListener: ItemSelectedListener +) : ListAdapter( object: DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SavedLocation, newItem: SavedLocation): Boolean { - return oldItem.title == newItem.title + return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: SavedLocation, newItem: SavedLocation): Boolean { return oldItem == newItem } }) { - inner class SavedLocationHolder( - itemView:View, - itemSelectedListener: OnItemSelectedListener - ) : RecyclerView.ViewHolder(itemView) { - val savedLocationXButton: ImageView by lazy{ - itemView.findViewById(R.id.savedLocationXButton) - } - val savedLocationTextView: TextView by lazy { - itemView.findViewById(R.id.savedLocationTextView) - } - - init { - itemView.setOnClickListener { - itemSelectedListener.onSavedLocationViewClicked(getItem(bindingAdapterPosition).title) - } - savedLocationXButton.setOnClickListener { - itemSelectedListener.onSavedLocationXButtonClicked(getItem(bindingAdapterPosition) as SavedLocation) - } - } + override fun onCreateViewHolder(parent:ViewGroup, viewType: Int): SavedLocationViewHolder { + val inflater = LayoutInflater.from(parent.context) + val itemSavedLocationBinding = ItemSavedLocationBinding.inflate(inflater, parent, false) + return SavedLocationViewHolder(itemSavedLocationBinding, itemSelectedListener) } - override fun onCreateViewHolder(parent:ViewGroup, viewType: Int): SavedLocationHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_saved_location, parent, false) - return SavedLocationHolder(view, itemSelectedListener) + override fun onBindViewHolder(holder:SavedLocationViewHolder, position: Int) { + holder.bind(getItem(position)) } - override fun onBindViewHolder(holder:SavedLocationHolder, position: Int) { - val savedLocation = getItem(position) - holder.savedLocationTextView.setText(savedLocation.title) + class SavedLocationViewHolder( + private val itemSavedLocationBinding: ItemSavedLocationBinding, + private val itemSelectedListener: ItemSelectedListener + ) : RecyclerView.ViewHolder(itemSavedLocationBinding.root) { + fun bind(item: SavedLocation) { // ViewHolder와 itemLocationBinding 연동 + itemSavedLocationBinding.savedLocation = item + itemSavedLocationBinding.onItemSelectedListener = itemSelectedListener + } } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/view/search/SearchClearButtonClickListener.kt b/app/src/main/java/campus/tech/kakao/map/view/search/SearchClearButtonClickListener.kt new file mode 100644 index 00000000..4dae1c4f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/view/search/SearchClearButtonClickListener.kt @@ -0,0 +1,5 @@ +package campus.tech.kakao.map.view.search + +interface SearchClearButtonClickListener { + fun onSearchClearButtonClicked() +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/LocationViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/LocationViewModel.kt index 5e4b8ca6..1709e627 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/LocationViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/LocationViewModel.kt @@ -5,10 +5,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import campus.tech.kakao.map.model.Location +import campus.tech.kakao.map.model.repository.DefaultLocationRepository import campus.tech.kakao.map.model.repository.LocationRepository +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class LocationViewModel( +@HiltViewModel +class LocationViewModel @Inject constructor( private val locationRepository: LocationRepository ) : ViewModel() { private val _locations = MutableLiveData>() @@ -27,9 +31,16 @@ class LocationViewModel( fun searchLocationsFromKakaoAPI(query: String, handleNoResultMessage: (Int) -> Unit) { viewModelScope.launch { - _searchedLocations.value = locationRepository.getLocationRemote(query) + _searchedLocations.value = locationRepository.getLocationAll(query) handleNoResultMessage(getSearchedLocationsSize()) } } + fun addLastLocation(location: Location){ + locationRepository.addLastLocation(location) + } + + fun getLastLocation(): Location? { + return locationRepository.getLastLocation() + } } \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedLocationViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedLocationViewModel.kt index 4d193c09..d35ff11c 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedLocationViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/SavedLocationViewModel.kt @@ -3,35 +3,49 @@ 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.model.SavedLocation +import campus.tech.kakao.map.model.repository.DefaultSavedLocationRepository import campus.tech.kakao.map.model.repository.SavedLocationRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class SavedLocationViewModel( +@HiltViewModel +class SavedLocationViewModel @Inject constructor( private val savedLocationRepository: SavedLocationRepository ) : ViewModel() { private val _savedLocation = MutableLiveData>() val savedLocation: LiveData> get() = _savedLocation fun setSavedLocation() { - _savedLocation.value = savedLocationRepository.getSavedLocationAll() + viewModelScope.launch { + _savedLocation.value = savedLocationRepository.getSavedLocationAll() + } } - fun addSavedLocation(title: String) { - val savedLocation = SavedLocation(title) - if(_savedLocation.value?.contains(savedLocation) == false){ - savedLocationRepository.addSavedLocation(title) - val currentList = _savedLocation.value ?: return - if (currentList.add(savedLocation)) { - _savedLocation.value = currentList + fun addSavedLocation(id: Long, title: String) { + val savedLocation = SavedLocation(id, title) + if (_savedLocation.value?.contains(savedLocation) == false) { + viewModelScope.launch { + savedLocationRepository.addSavedLocation(savedLocation) + val currentList = _savedLocation.value + if (currentList != null) { + currentList.add(savedLocation) + _savedLocation.value = currentList + } } } } - fun deleteSavedLocation(savedLocation: SavedLocation) { - if (savedLocationRepository.deleteSavedLocation(savedLocation.title)) { - val currentList = _savedLocation.value ?: return - if (currentList.remove(savedLocation)) { + fun deleteSavedLocation(savedLocation: SavedLocation) { + viewModelScope.launch { + savedLocationRepository.deleteSavedLocation(savedLocation) + val currentList = _savedLocation.value + if (currentList != null) { + currentList.remove(savedLocation) _savedLocation.value = currentList } } + } } diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/ViewModelFactory.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/ViewModelFactory.kt deleted file mode 100644 index 4667404c..00000000 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/ViewModelFactory.kt +++ /dev/null @@ -1,33 +0,0 @@ -package campus.tech.kakao.map.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import campus.tech.kakao.map.model.repository.LocationRepository -import campus.tech.kakao.map.model.repository.SavedLocationRepository - -class ViewModelFactory { - class LocationViewModelFactory( - private val locationRepository: LocationRepository - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class, extras: CreationExtras): T { - if (modelClass.isAssignableFrom(LocationViewModel::class.java)) { - return LocationViewModel(locationRepository) as T - } else { - throw IllegalArgumentException() - } - } - } - - class SavedLocationViewModelFactory( - private val savedLocationRepository: SavedLocationRepository - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class, extras: CreationExtras): T { - if (modelClass.isAssignableFrom(SavedLocationViewModel::class.java)) { - return SavedLocationViewModel(savedLocationRepository) as T - } else { - throw IllegalArgumentException() - } - } - } -} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 26c722b8..d8cba374 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,4 +1,10 @@ + + + + + android:layout_marginRight="10dp" + android:onClick="@{() -> searchClickListener.onSearchClearButtonClicked()}"/> @@ -53,15 +60,16 @@ app:layout_constraintTop_toBottomOf="@id/savedLocationRecyclerView" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml index 940e1691..eee4a65b 100644 --- a/app/src/main/res/layout/activity_map.xml +++ b/app/src/main/res/layout/activity_map.xml @@ -17,7 +17,7 @@ app:layout_constraintTop_toBottomOf="parent" /> @@ -48,6 +48,7 @@ app:layout_constraintStart_toStartOf="parent"> diff --git a/app/src/main/res/layout/item_location.xml b/app/src/main/res/layout/item_location.xml index f476bbb0..1d41324c 100644 --- a/app/src/main/res/layout/item_location.xml +++ b/app/src/main/res/layout/item_location.xml @@ -1,49 +1,62 @@ - - - - - - - - - + + + + + + + + android:padding="10dp" + android:onClick="@{() -> onItemSelectedListener.onLocationItemClicked(location)}"> + + + + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_saved_location.xml b/app/src/main/res/layout/item_saved_location.xml index 80af92bd..62bfeb26 100644 --- a/app/src/main/res/layout/item_saved_location.xml +++ b/app/src/main/res/layout/item_saved_location.xml @@ -1,29 +1,41 @@ - - - + + + + + + android:padding="10dp" + android:onClick="@{() -> onItemSelectedListener.onSavedLocationItemClicked(savedLocation.title)}"> + - + - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/map_bottom_sheet.xml b/app/src/main/res/layout/map_bottom_sheet.xml index aabaa4b7..e6decc21 100644 --- a/app/src/main/res/layout/map_bottom_sheet.xml +++ b/app/src/main/res/layout/map_bottom_sheet.xml @@ -2,7 +2,7 @@ \ No newline at end of file diff --git a/app/src/test/java/campus/tech/kakao/map/SavedLocationDataSourceTest.kt b/app/src/test/java/campus/tech/kakao/map/SavedLocationRemoteDataSourceTest.kt similarity index 93% rename from app/src/test/java/campus/tech/kakao/map/SavedLocationDataSourceTest.kt rename to app/src/test/java/campus/tech/kakao/map/SavedLocationRemoteDataSourceTest.kt index 4726ee2c..5f677c9e 100644 --- a/app/src/test/java/campus/tech/kakao/map/SavedLocationDataSourceTest.kt +++ b/app/src/test/java/campus/tech/kakao/map/SavedLocationRemoteDataSourceTest.kt @@ -3,9 +3,7 @@ package campus.tech.kakao.map import android.database.sqlite.SQLiteDatabase import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import campus.tech.kakao.map.model.LocationDbHelper import campus.tech.kakao.map.model.SavedLocation -import campus.tech.kakao.map.model.datasource.SavedLocationDataSource import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before @@ -13,7 +11,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class SavedLocationDataSourceTest { +class SavedLocationRemoteDataSourceTest { private lateinit var locationLocalDataSource: SavedLocationDataSource private lateinit var locationDbHelper: LocationDbHelper private lateinit var sqLiteDatabase: SQLiteDatabase diff --git a/build.gradle.kts b/build.gradle.kts index 6465d171..d6b55841 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.android.application") version "8.3.1" apply false id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false + id("com.google.dagger.hilt.android") version "2.48.1" apply false } allprojects {