diff --git a/README.md b/README.md index 64c3bb5d..f091e3fc 100644 --- a/README.md +++ b/README.md @@ -1 +1,27 @@ -# android-map-location \ No newline at end of file +# android-map-location +## step1 기능 목록 +1. 저장된 검색어 목록에 기능 추가하기 + - 저장된 검색어 중 하나를 선택하면 해당 검색어의 검색 결과 목록이 표시된다. +2. 검색 결과 목록에 기능 추가하기 + - 검색 결과 목록 중 하나의 항목을 선택하면 해당 항목의 위치를 지도에 표시한다. +3. 마지막 위치 저장하기 + - 앱 종료 시 마지막 위치를 SharedPreference 저장하여 다시 앱 실행 시 해당 위치로 포커스 한다. +4. 에러 처리하기 + - 카카오지도 onMapError() 호출 시 에러 화면을 보여준다. "지도 인증을 실패했습니다. 다시 시도해주세요. '에러이름(에러코드): 에러메세지' +## step2 기능 목록 +- 테스트 코드 - JUnit과 mockito를 이용하여 단위 테스트 코드를 작성한다. + - data source 테스트 + - local + - SavedLocation 저장 테스트 + - SavedLocation 삭제 테스트 + - SavedLocation 조회 테스트 + - 마지막 위치 저장 테스트 + - UI 테스트 코드 + - 검색 페이지 + - 검색 결과 목록 테스트 + - 검색어 저장 목록 테스트 + - clearButton 테스트 + - 지도 페이지 + - BottomSheet 테스트 + - KakaoMap Label 테스트 + - 지도 에러 화면 테스트 \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01a3c017..90173142 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,9 +59,16 @@ dependencies { implementation("androidx.datastore:datastore-preferences:1.0.0") implementation("com.android.identity:identity-credential-android:20231002") implementation("androidx.activity:activity:1.9.0") + implementation("androidx.test:core-ktx:1.6.1") + testImplementation("androidx.test.ext:junit:1.1.1") + testImplementation ("org.robolectric:robolectric:4.9") + testImplementation("junit:junit:4.13.2") + testImplementation("org.mockito:mockito-core:5.8.0") + androidTestImplementation("org.mockito:mockito-core:5.8.0") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation("androidx.test.espresso:espresso-contrib:3.2.0") implementation("androidx.activity:activity-ktx:1.1.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") @@ -71,4 +78,5 @@ dependencies { // retrofit 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") } diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MainActivityUiTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MainActivityUiTest.kt new file mode 100644 index 00000000..ee6a22f0 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MainActivityUiTest.kt @@ -0,0 +1,61 @@ +package campus.tech.kakao.map + +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 { + @get: Rule + var activityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + + @Test + fun `검색창에_검색어를_입력하고_clearButton을_클릭하면_입력한_검색어가_삭제된다`() { + onView(withId(R.id.SearchEditTextInMain)).perform(replaceText("카페")).check(matches(withText("카페"))) + onView(withId(R.id.clearButton)).perform(click()) + onView(withId(R.id.SearchEditTextInMain)).check(matches(withText(""))) + } + + @Test + fun `리싸이클러뷰가_화면에_표시된다`() { + onView(withId(R.id.locationRecyclerView)).check(matches(isDisplayed())) + } + + @Test + fun `검색창에_부산대를_입력하고_리싸이클러뷰의_0번_아이템을_클릭하면_지도와_BottomSheet가_화면에_표시된다`() { + Intents.init() + onView(withId(R.id.SearchEditTextInMain)).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.map_view)).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MapActivityUiTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityUiTest.kt new file mode 100644 index 00000000..c98d2d8a --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityUiTest.kt @@ -0,0 +1,29 @@ +package campus.tech.kakao.map + +import androidx.test.espresso.Espresso.onView +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 androidx.test.ext.junit.runners.AndroidJUnit4 +import campus.tech.kakao.map.view.map.MapActivity +import com.kakao.vectormap.KakaoMap +import com.kakao.vectormap.KakaoMapSdk +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@RunWith(AndroidJUnit4::class) +class MapActivityUiTest { + @get: Rule + var activityRule = ActivityScenarioRule(MapActivity::class.java) + + @Test + fun `지도에서_에러_발생_시_에러_메세지가_나타난다`() { // 테스트를 통과하지 않습니다.. 지도 에러를 발생시키는 부분에서 오류가 나는 것 같아요ㅠㅠ 어렵습니다... + activityRule.scenario.onActivity { activity -> + KakaoMapSdk.init(activity, "fakeKey"); + } + onView(withId(R.id.errorMessageTextView)).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e5c8910d..278d536b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ { - return withContext(Dispatchers.IO){ val response = client.searchFromKeyword(keyword, RESULT_SIZE) val locationDtos: List = response.body()?.documents ?: emptyList() diff --git a/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationLocalDataSource.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationLocalDataSource.kt deleted file mode 100644 index e55559a1..00000000 --- a/app/src/main/java/campus/tech/kakao/map/model/datasource/LocationLocalDataSource.kt +++ /dev/null @@ -1,141 +0,0 @@ -package campus.tech.kakao.map.model.datasource - -import android.content.ContentValues -import android.util.Log -import campus.tech.kakao.map.model.Contract.LocationEntry -import campus.tech.kakao.map.model.Contract.SavedLocationEntry -import campus.tech.kakao.map.model.Location -import campus.tech.kakao.map.model.LocationDbHelper -import campus.tech.kakao.map.model.SavedLocation - -class LocationLocalDataSource(private val dbHelper : LocationDbHelper) { - - fun addLocation(title: String, address: String, category: String): Long { - val db = dbHelper.writableDatabase - - val values = ContentValues().apply { - put(LocationEntry.COLUMN_NAME_TITLE, title) - put(LocationEntry.COLUMN_NAME_ADDRESS, address) - put(LocationEntry.COLUMN_NAME_CATEGORY, category) - } - - return db.insert(LocationEntry.TABLE_NAME, null, values) - } - - fun getLocations(): MutableList { - val db = dbHelper.readableDatabase - - val projection = arrayOf( - LocationEntry.COLUMN_NAME_TITLE, - LocationEntry.COLUMN_NAME_ADDRESS, - LocationEntry.COLUMN_NAME_CATEGORY - ) - - val sortOrder = "${LocationEntry.COLUMN_NAME_TITLE} ASC" - - val cursor = db.query( - LocationEntry.TABLE_NAME, - projection, - null, - null, - null, - null, - sortOrder - ) - - val results = mutableListOf() - with(cursor) { - while (moveToNext()) { - val title = getString(getColumnIndexOrThrow(LocationEntry.COLUMN_NAME_TITLE)) - val address = getString(getColumnIndexOrThrow(LocationEntry.COLUMN_NAME_ADDRESS)) - val category = getString(getColumnIndexOrThrow(LocationEntry.COLUMN_NAME_CATEGORY)) - results.add(Location(title, address, category)) - } - } - cursor.close() - return results - } - - 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) - } - - fun searchLocation(query: String): List { - if(query.isBlank()){ - return emptyList() - } - val db = dbHelper.readableDatabase - - val projection = arrayOf( - LocationEntry.COLUMN_NAME_TITLE, - LocationEntry.COLUMN_NAME_ADDRESS, - LocationEntry.COLUMN_NAME_CATEGORY - ) - - val selection = "${LocationEntry.COLUMN_NAME_TITLE} LIKE '%' || ? || '%' OR ${LocationEntry.COLUMN_NAME_ADDRESS} LIKE '%' || ? || '%' OR ${LocationEntry.COLUMN_NAME_CATEGORY} LIKE '%' || ? || '%'" - val selectionArgs = arrayOf(query, query, query) - - val cursor = db.query( - LocationEntry.TABLE_NAME, - projection, - selection, - selectionArgs, - null, - null, - null - ) - val results = mutableListOf() - with(cursor) { - while (moveToNext()) { - val title = getString(getColumnIndexOrThrow(LocationEntry.COLUMN_NAME_TITLE)) - val address = getString(getColumnIndexOrThrow(LocationEntry.COLUMN_NAME_ADDRESS)) - val category = getString(getColumnIndexOrThrow(LocationEntry.COLUMN_NAME_CATEGORY)) - results.add(Location(title, address, category)) - } - } - cursor.close() - return results - } -} 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 new file mode 100644 index 00000000..9310c1e8 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/SavedLocationDataSource.kt @@ -0,0 +1,56 @@ +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/SharedPreferences.kt b/app/src/main/java/campus/tech/kakao/map/model/datasource/SharedPreferences.kt new file mode 100644 index 00000000..8ccd7f09 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/datasource/SharedPreferences.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map.model.datasource + +import android.content.Context + +class SharedPreferences(context: Context) { + private val prefs = context.getSharedPreferences("myPref", Context.MODE_PRIVATE) + + fun getString(key: String, defValue: String): String { + return prefs.getString(key, defValue).toString() + } + + fun putString(key: String, value: String) { + prefs.edit().putString(key, value).apply() + + } +} \ 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 new file mode 100644 index 00000000..01024b00 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/repository/LastLocationRepository.kt @@ -0,0 +1,16 @@ +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 dd3b8d4d..b8846592 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,32 +1,12 @@ package campus.tech.kakao.map.model.repository import campus.tech.kakao.map.model.Location -import campus.tech.kakao.map.model.datasource.LocationLocalDataSource -import campus.tech.kakao.map.model.datasource.LocationRemoteDataSource +import campus.tech.kakao.map.model.datasource.LocationDataSource class LocationRepository( - private val locationLocalRepository: LocationLocalDataSource, - private val locationRemoteRepository: LocationRemoteDataSource + private val locationRemoteDataSource: LocationDataSource ) { - fun addLocationLocal() { - for (i in 1..9) { - locationLocalRepository.addLocation("카페$i", "부산 부산진구 전포대로$i", "카페") - } - for (i in 1..9) { - locationLocalRepository.addLocation("음식점$i", "부산 부산진구 중앙대로$i", "음식점") - } - } - - fun searchLocationLocal(query: String): List { - val results = locationLocalRepository.searchLocation(query) - return if (results.isNotEmpty()) results else emptyList() - } - - fun getLocationLocal(): MutableList { - return locationLocalRepository.getLocations() - } - suspend fun getLocationRemote(query: String): List { - return locationRemoteRepository.getLocations(query) + return locationRemoteDataSource.getLocations(query) } } \ 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 1b47f435..e4996d97 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,10 +1,10 @@ package campus.tech.kakao.map.model.repository import campus.tech.kakao.map.model.SavedLocation -import campus.tech.kakao.map.model.datasource.LocationLocalDataSource +import campus.tech.kakao.map.model.datasource.SavedLocationDataSource class SavedLocationRepository( - private val locationLocalRepository: LocationLocalDataSource + private val locationLocalRepository: SavedLocationDataSource ) { fun getSavedLocationAll(): MutableList { val results = locationLocalRepository.getSavedLocationAll() 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 e959b368..c1f66493 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 @@ -1,35 +1,63 @@ package campus.tech.kakao.map.view.map import android.content.Intent +import android.graphics.Color import android.os.Bundle import android.util.Log +import android.view.View import android.widget.EditText +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import campus.tech.kakao.map.BuildConfig +import androidx.constraintlayout.widget.ConstraintLayout import campus.tech.kakao.map.R +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 com.google.android.material.bottomsheet.BottomSheetBehavior import com.kakao.vectormap.KakaoMap import com.kakao.vectormap.KakaoMapReadyCallback -import com.kakao.vectormap.KakaoMapSdk +import com.kakao.vectormap.LatLng import com.kakao.vectormap.MapLifeCycleCallback import com.kakao.vectormap.MapView +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles class MapActivity : AppCompatActivity() { - private lateinit var searchEditText: EditText - private lateinit var mapView: MapView + 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 lastLocationLocalDataSource: LastLocationlDataSource by lazy { LastLocationlDataSource() } + private val lastLocationRepository: LastLocationRepository by lazy { LastLocationRepository(lastLocationLocalDataSource) } + + companion object{ + private val DEFAULT_LONGITUDE = 127.115587 + private val DEFAULT_LATITUDE = 37.406960 + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_map) - initViews() setupEditText() setupMapView() } - private fun initViews() { - searchEditText = findViewById(R.id.SearchEditTextInMap) - mapView = findViewById(R.id.map_view) + override fun onResume() { + super.onResume() + mapView.resume() // MapView 의 resume 호출 + } + + override fun onPause() { + super.onPause() + mapView.pause() // MapView 의 pause 호출 } private fun setupEditText() { @@ -40,22 +68,97 @@ class MapActivity : AppCompatActivity() { } private fun setupMapView() { - KakaoMapSdk.init(this, BuildConfig.KAKAO_API_KEY); mapView.start(object : MapLifeCycleCallback() { override fun onMapDestroy() { - // 지도 API 가 정상적으로 종료될 때 호출됨 Log.d("jieun", "onMapDestroy") } - override fun onMapError(error: Exception) { - // 인증 실패 및 지도 사용 중 에러가 발생할 때 호출됨 + override fun onMapError(error: Exception) { // 인증 실패 및 지도 사용 중 에러가 발생할 때 호출됨 Log.d("jieun", "onMapError" + error) + showErrorMessage(error) } }, object : KakaoMapReadyCallback() { - override fun onMapReady(kakaoMap: KakaoMap) { - // 인증 후 API 가 정상적으로 실행될 때 호출됨 - Log.d("jieun", "onMapReady") + val location = getCoordinates() + override fun onMapReady(kakaoMap: KakaoMap) { // 인증 후 API 가 정상적으로 실행될 때 호출됨 + Log.d("jieun", "onMapReady coordinates: " + location.toString()) + if (location != null) { + showLabel(location, kakaoMap) + showBottomSheet(location) + lastLocationRepository.putLastLocation(location) +// Log.d("jieun", "onMapReady setSharedData: " + getSharedData("pref")) + } else{ + hideBottomSheet() + } } + + override fun getPosition(): LatLng { +// Log.d("jieun", "getPosition coordinates: " + coordinates.toString()) + if (location != null) { + return LatLng.from(location.latitude, location.longitude) + } else{ + return LatLng.from(DEFAULT_LATITUDE, DEFAULT_LONGITUDE) + } + + } + }) } + + private fun showErrorMessage(error: Exception) { + runOnUiThread { + setContentView(R.layout.error_map) + errorMessageTextView.text = "지도 인증을 실패했습니다.\n다시 시도해주세요.\n\n" + error.message + } + } + + private fun showLabel( + location: Location, + kakaoMap: KakaoMap + ) { + val labelStyles: LabelStyles = LabelStyles.from( + LabelStyle.from(R.drawable.location_red_icon_resized).setZoomLevel(8), + LabelStyle.from(R.drawable.location_red_icon_resized) + .setTextStyles(32, Color.BLACK, 1, Color.GRAY).setZoomLevel(15) + ) + val position = LatLng.from(location.latitude, location.longitude) + kakaoMap.labelManager?.getLayer()?.addLabel( + LabelOptions.from(position) + .setStyles(labelStyles) + .setTexts(location.title) + ) + } + + private fun hideBottomSheet() { + bottomSheetLayout.visibility = View.GONE + } + + private fun showBottomSheet(location: Location) { + bottomSheetLayout.visibility = View.VISIBLE + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + bottom_sheet_title.text = location.title + bottom_sheet_address.text = location.address + } + + private fun getCoordinates(): Location? { + var location = getCoordinatesByIntent() + if(location == null) { + location = lastLocationRepository.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 + } } \ 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 8472b3be..07d26ada 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 @@ -32,7 +32,8 @@ class LocationAdapter( init { itemView.setOnClickListener { - itemSelectedListener.addSavedLocation(getItem(bindingAdapterPosition).title) + val location = getItem(bindingAdapterPosition) + itemSelectedListener.onLocationViewClicked(location) } } } 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 56ab33f7..f0d1bca5 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 @@ -1,5 +1,6 @@ package campus.tech.kakao.map.view.search +import android.content.Intent import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -12,82 +13,66 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import campus.tech.kakao.map.BuildConfig -import campus.tech.kakao.map.model.datasource.LocationLocalDataSource -import campus.tech.kakao.map.model.datasource.LocationRemoteDataSource +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.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 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 lateinit var locationViewModel: LocationViewModel - private lateinit var locationAdapter: LocationAdapter - private lateinit var locationRecyclerView: RecyclerView + private val savedLocationViewModel: SavedLocationViewModel by lazy { + ViewModelProvider(this, SavedLocationViewModelFactory(savedLocationRepository)) + .get(SavedLocationViewModel::class.java) + } - private lateinit var savedLocationViewModel: SavedLocationViewModel - private lateinit var savedLocationAdapter: SavedLocationAdapter - private lateinit var savedLocationRecyclerView: RecyclerView + private val savedLocationAdapter: SavedLocationAdapter by lazy { SavedLocationAdapter(this) } + private val savedLocationRecyclerView: RecyclerView by lazy { + findViewById(R.id.savedLocationRecyclerView) + } - private lateinit var locationDbHelper: LocationDbHelper - private lateinit var locationLocalDataSource: LocationLocalDataSource - private lateinit var locationRemoteDataSource: LocationRemoteDataSource - private lateinit var locationRepository: LocationRepository - private lateinit var savedLocationRepository: SavedLocationRepository + 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 lateinit var clearButton: ImageView - private lateinit var searchEditText: EditText - private lateinit var noResultTextView: TextView + 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) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - initViews() setupSearchEditText() setupClearButton() setupViewModels() setupRecyclerViews() } - private fun initViews() { - locationDbHelper = LocationDbHelper(this) - locationLocalDataSource = LocationLocalDataSource(locationDbHelper) - locationRemoteDataSource = LocationRemoteDataSource() - locationRepository = LocationRepository(locationLocalDataSource, locationRemoteDataSource) - savedLocationRepository = SavedLocationRepository(locationLocalDataSource) - - locationViewModel = ViewModelProvider(this, LocationViewModelFactory(locationRepository)) - .get(LocationViewModel::class.java) - locationRecyclerView = findViewById(R.id.locationRecyclerView) - - savedLocationViewModel = ViewModelProvider(this, SavedLocationViewModelFactory(savedLocationRepository)) - .get(SavedLocationViewModel::class.java) - savedLocationRecyclerView = findViewById(R.id.savedLocationRecyclerView) - - clearButton = findViewById(R.id.clearButton) - searchEditText = findViewById(R.id.SearchEditTextInMain) - noResultTextView = findViewById(R.id.NoResultTextView) - } - private fun setupSearchEditText() { searchEditText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { val query = s.toString() - locationViewModel.searchLocationsFromKakaoAPI(query) {searchLocationsSize -> - if (searchLocationsSize > 0) { - noResultTextView.visibility = View.GONE - } else { - noResultTextView.visibility = View.VISIBLE - } + locationViewModel.searchLocationsFromKakaoAPI(query) { searchLocationsSize -> + handleNoResultMessage(searchLocationsSize) } } @@ -128,21 +113,47 @@ class MainActivity : AppCompatActivity(), OnItemSelectedListener { } private fun setupRecyclerViews() { - locationAdapter = LocationAdapter(this) locationRecyclerView.layoutManager = LinearLayoutManager(this) locationRecyclerView.adapter = locationAdapter - savedLocationAdapter = SavedLocationAdapter(this) savedLocationRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) savedLocationRecyclerView.adapter = savedLocationAdapter } - override fun addSavedLocation(title: String) { - savedLocationViewModel.addSavedLocation(title) + override fun onLocationViewClicked(location: Location) { + savedLocationViewModel.addSavedLocation(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) + startActivity(intent) } - override fun deleteSavedLocation(item: SavedLocation) { + override fun onSavedLocationXButtonClicked(item: SavedLocation) { savedLocationViewModel.deleteSavedLocation(item) } + + override fun onSavedLocationViewClicked(title: String) { + updateEditText(title) + locationViewModel.searchLocationsFromKakaoAPI(title){ searchLocationsSize -> + handleNoResultMessage(searchLocationsSize) + } + } + + private fun updateEditText(title: String) { + searchEditText.setText(title) + searchEditText.setSelection(searchEditText.text.length) + } + + private fun handleNoResultMessage(searchLocationsSize: Int) { + if (searchLocationsSize > 0) { + noResultTextView.visibility = View.GONE + } else { + 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 index 432f2090..ddd4e8d3 100644 --- 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 @@ -1,8 +1,10 @@ package campus.tech.kakao.map.view.search +import campus.tech.kakao.map.model.Location import campus.tech.kakao.map.model.SavedLocation interface OnItemSelectedListener { - fun addSavedLocation(title: String) - fun deleteSavedLocation(item: SavedLocation) + 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 cd8f60d0..97335afb 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 @@ -27,12 +27,19 @@ class SavedLocationAdapter( itemView:View, itemSelectedListener: OnItemSelectedListener ) : RecyclerView.ViewHolder(itemView) { - val savedLocationXButton: ImageView = itemView.findViewById(R.id.savedLocationXButton) - val savedLocationTextView: TextView = itemView.findViewById(R.id.savedLocationTextView) + 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.deleteSavedLocation(getItem(bindingAdapterPosition) as SavedLocation) + itemSelectedListener.onSavedLocationXButtonClicked(getItem(bindingAdapterPosition) as SavedLocation) } } } 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 f3e4b0a9..5e4b8ca6 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 @@ -16,33 +16,20 @@ class LocationViewModel( private val _searchedLocations = MutableLiveData>() val searchedLocations: LiveData> get() = _searchedLocations - fun setLocationsFromDB() { - _locations.value = locationRepository.getLocationLocal() - _searchedLocations.value = emptyList() - } - private fun getSearchedLocationsSize(): Int { return _searchedLocations.value?.size ?: 0 } - - fun searchLocationsFromDB(query: String): Int { - return locationRepository.searchLocationLocal(query).size - } - - fun addLocationFromDB() { - locationRepository.addLocationLocal() - } - fun setLocationsFromKakaoAPI() { _locations.value = emptyList() _searchedLocations.value = emptyList() } - fun searchLocationsFromKakaoAPI(query: String, deleteNoResultMessageCallback: (Int) -> Unit) { + fun searchLocationsFromKakaoAPI(query: String, handleNoResultMessage: (Int) -> Unit) { viewModelScope.launch { _searchedLocations.value = locationRepository.getLocationRemote(query) - deleteNoResultMessageCallback(getSearchedLocationsSize()) + handleNoResultMessage(getSearchedLocationsSize()) } } + } \ No newline at end of file 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 index a514504f..4667404c 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/ViewModelFactory.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/ViewModelFactory.kt @@ -3,7 +3,6 @@ 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.datasource.LocationLocalDataSource import campus.tech.kakao.map.model.repository.LocationRepository import campus.tech.kakao.map.model.repository.SavedLocationRepository diff --git a/app/src/main/res/drawable/location_red_icon_resized.png b/app/src/main/res/drawable/location_red_icon_resized.png new file mode 100644 index 00000000..1f2b57bd Binary files /dev/null and b/app/src/main/res/drawable/location_red_icon_resized.png differ diff --git a/app/src/main/res/drawable/map_background_bottom_sheet.xml b/app/src/main/res/drawable/map_background_bottom_sheet.xml new file mode 100644 index 00000000..798be23e --- /dev/null +++ b/app/src/main/res/drawable/map_background_bottom_sheet.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ 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 39747345..940e1691 100644 --- a/app/src/main/res/layout/activity_map.xml +++ b/app/src/main/res/layout/activity_map.xml @@ -40,4 +40,16 @@ android:layout_marginRight="10dp" android:elevation="4dp"/> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/error_map.xml b/app/src/main/res/layout/error_map.xml new file mode 100644 index 00000000..d392d7bb --- /dev/null +++ b/app/src/main/res/layout/error_map.xml @@ -0,0 +1,19 @@ + + + + + + \ 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 new file mode 100644 index 00000000..aabaa4b7 --- /dev/null +++ b/app/src/main/res/layout/map_bottom_sheet.xml @@ -0,0 +1,54 @@ + + + + + + + + + + \ 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/SavedLocationDataSourceTest.kt new file mode 100644 index 00000000..4726ee2c --- /dev/null +++ b/app/src/test/java/campus/tech/kakao/map/SavedLocationDataSourceTest.kt @@ -0,0 +1,71 @@ +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 +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SavedLocationDataSourceTest { + private lateinit var locationLocalDataSource: SavedLocationDataSource + private lateinit var locationDbHelper: LocationDbHelper + private lateinit var sqLiteDatabase: SQLiteDatabase + private lateinit var savedLocations: List + + @Before + fun init() { + locationDbHelper = LocationDbHelper(ApplicationProvider.getApplicationContext()) + locationLocalDataSource = SavedLocationDataSource(locationDbHelper) + + savedLocations = listOf( + SavedLocation("Location 1"), + SavedLocation("Location 2"), + SavedLocation("Location 3") + ) + + sqLiteDatabase = locationDbHelper.writableDatabase + savedLocations.forEach { + locationLocalDataSource.addSavedLocation(it.title) + } + } + + @Test + fun `사용자가_선택한_위치가_데이터베이스에_저장된다`() { + val newLocationTitle = "New Location" + val result: Long = locationLocalDataSource.addSavedLocation(newLocationTitle) + assertEquals(4L, result) + } + + @Test + fun `데이터베이스에_저장된_위치를_모두_불러온다`() { + val result = locationLocalDataSource.getSavedLocationAll() + assertEquals(savedLocations.size, result.size) + for (i in savedLocations.indices) { + assertEquals(savedLocations[i].title, result[i].title) + } + } + + @Test + fun `저장된_위치를_삭제한다`() { + val titleToDelete = savedLocations[0].title + val result = locationLocalDataSource.deleteSavedLocation(titleToDelete) + assertEquals(1, result) + + // 삭제 후 확인 + val results = locationLocalDataSource.getSavedLocationAll() + assertEquals(savedLocations.size - 1, results.size) + } + + @After + fun tearDown() { + // 데이터베이스 정리 + locationDbHelper.close() + } +} \ No newline at end of file