Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 Android 박정훈 4주차 과제 Step2 #93

Open
wants to merge 14 commits into
base: pjhn
Choose a base branch
from
Open
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
# android-map-location

카카오 맵 클론 코딩
카카오로컬 API 사용

## 기능 요구 사항
- 저장된 검색어를 선택하면 해당 검색어의 검색 결과가 표시된다.
- 검색 결과 목록 중 하나의 항목을 선택하면 해당 항목의 위치를 지도에 표시한다.
- 앱 종료 시 마지막 위치를 저장하여 다시 앱 실행 시 해당 위치로 포커스 한다.
- 카카오지도 onMapError() 호출 시 에러 화면을 보여준다.
-
## 프로그래밍 요구 사항
- BottomSheet를 사용한다.
- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다.
- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.


42 changes: 29 additions & 13 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
}

android {
namespace = "campus.tech.kakao.map"
compileSdk = 34


defaultConfig {
resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY"))
buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))
applicationId = "campus.tech.kakao.map"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
ndk {
abiFilters.add("arm64-v8a")
abiFilters.add("armeabi-v7a")}


testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -36,28 +46,34 @@ android {

buildFeatures {
viewBinding = true
dataBinding = true
buildConfig = true
}
}

dependencies {

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
implementation("com.kakao.sdk:v2-all:2.20.3")
implementation("com.kakao.maps.open:android:2.9.5")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.kakao.maps.open:android:2.9.5")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.activity:activity:1.8.0")
implementation("androidx.test:core-ktx:1.5.0")
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk-android:1.13.11")
testImplementation("io.mockk:mockk-agent:1.13.11")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("org.robolectric:robolectric:4.11.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
testImplementation("io.mockk:mockk:1.13.12")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
implementation("androidx.test:core-ktx:1.6.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.3.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.3.0")
}

fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
21 changes: 21 additions & 0 deletions app/build.gradle.kts.rej
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
diff a/app/build.gradle.kts b/app/build.gradle.kts (rejected hunks)
@@ -69,10 +69,17 @@
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.datastore:datastore-preferences:1.0.0")
- implementation("androidx.activity:activity:1.8.0")
+ implementation("androidx.activity:activity-ktx:1.8.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation("androidx.test:rules:1.4.0")
+ androidTestImplementation("androidx.test:runner:1.4.0")
+ androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
+ androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
+ androidTestImplementation("io.mockk:mockk-android:1.13.3")
+ androidTestImplementation("androidx.arch.core:core-testing:2.1.0")
}

+
fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
\ No newline at end of file
37 changes: 37 additions & 0 deletions app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package campus.tech.kakao.map

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
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.MapActivity
import campus.tech.kakao.map.view.ViewActivity
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MapActivityTest {
@get:Rule
var activityScenarioRule = ActivityScenarioRule(MapActivity::class.java)

@Test
fun testActivityLaunch() {

onView(withId(R.id.mapView)).check(matches(isDisplayed()))
onView(withId(R.id.searchView)).check(matches(isDisplayed()))
}

@Test
fun testSearchedResultOnMap() {
Intents.init()
onView(withId(R.id.searchView)).perform(click())
Intents.intended(IntentMatchers.hasComponent(ViewActivity::class.java.name))
Intents.release()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package campus.tech.kakao.map

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import campus.tech.kakao.map.data.PlaceRepositoryImpl
import campus.tech.kakao.map.domain.model.Place
import campus.tech.kakao.map.util.PlaceContract
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 RepositoryImplTest {

private lateinit var repository: PlaceRepositoryImpl
private lateinit var context: Context

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
context.deleteDatabase(PlaceContract.DATABASE_NAME)
repository = PlaceRepositoryImpl.getInstance(context)
}

@After
fun after() {
repository.close()
context.deleteDatabase(PlaceContract.DATABASE_NAME)
}

@Test
fun testInsertAndGetPlaces() {
val place1 = Place("1", "Place1", "Address1", "Category1", "10.0", "20.0")
val place2 = Place("2", "Place2", "Address2", "Category2", "30.0", "40.0")
val places = listOf(place1, place2)

repository.updatePlaces(places)

val result = repository.getAllPlaces()
assertEquals(2, result.size)
assertEquals("Place1", result[0].place)
assertEquals("Place2", result[1].place)
}

@Test
fun testSearchPlaces() {
val place1 = Place("1", "Gangnam", "Address1", "Category1", "10.0", "20.0")
val place2 = Place("2", "Gangbuk", "Address2", "Category2", "30.0", "40.0")
val places = listOf(place1, place2)

repository.updatePlaces(places)

val result = repository.getPlaces("Gang")
assertEquals(2, result.size)
assertEquals("Gangnam", result[0].place)
assertEquals("Gangbuk", result[1].place)
}

@Test
fun testLogs() {
val log1 = Place("1", "Log1", "", "", "", "")
val log2 = Place("2", "Log2", "", "", "", "")
val logs = listOf(log1, log2)

repository.updateLogs(logs)

var result = repository.getLogs()
assertEquals(2, result.size)
assertEquals("Log1", result[0].place)
assertEquals("Log2", result[1].place)

repository.removeLog("1")
result = repository.getLogs()
assertEquals(1, result.size)
assertEquals("Log2", result[0].place)
}
}
71 changes: 71 additions & 0 deletions app/src/androidTest/java/campus/tech/kakao/map/ViewActivityTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package campus.tech.kakao.map

import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import campus.tech.kakao.map.R
import campus.tech.kakao.map.domain.model.Place
import campus.tech.kakao.map.view.MapActivity
import campus.tech.kakao.map.view.ViewActivity
import campus.tech.kakao.map.view.adapter.SearchedPlaceAdapter
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.Matchers.allOf
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ViewActivityTest {

private lateinit var sharedPreferences: SharedPreferences
private lateinit var context: Context

@get:Rule
var activityScenarioRule = ActivityScenarioRule(ViewActivity::class.java)

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
sharedPreferences = context.getSharedPreferences("mockk", Context.MODE_PRIVATE)
Intents.init()
}

@After
fun after() {
sharedPreferences.edit().clear().apply()
Intents.release()
}

@Test
fun testSearchAndVerifyMapActivityLaunched() {
onView(withId(R.id.edtSearch)).perform(click()).perform(replaceText("부산대"))

Thread.sleep(1200L)
onView(withId(R.id.recyclerPlace))
.perform(
RecyclerViewActions.actionOnHolderItem(
instanceOf(SearchedPlaceAdapter.LocationViewHolder::class.java), click()
).atPosition(3)
)

Intents.intended(hasComponent(MapActivity::class.java.name))
}
}
15 changes: 13 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".PlaceApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -13,14 +16,22 @@
android:theme="@style/Theme.Map"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".view.MapActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".view.PlaceViewModel"
android:exported="false" />
<activity
android:name=".view.ViewActivity"
android:exported="false"/>


</application>

</manifest>
</manifest>
11 changes: 0 additions & 11 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt

This file was deleted.

34 changes: 34 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/PlaceApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package campus.tech.kakao.map

import android.app.Application
import campus.tech.kakao.map.data.PlaceRepositoryImpl
import campus.tech.kakao.map.domain.repository.PlaceRepository
import campus.tech.kakao.map.domain.use_case.GetAllPlacesUseCase
import campus.tech.kakao.map.domain.use_case.GetLogsUseCase
import campus.tech.kakao.map.domain.use_case.GetPlacesUseCase
import campus.tech.kakao.map.domain.use_case.UpdateLogsUseCase
import campus.tech.kakao.map.domain.use_case.RemoveLogUseCase
import campus.tech.kakao.map.domain.use_case.UpdatePlacesUseCase
import com.kakao.vectormap.KakaoMapSdk

class PlaceApplication: Application() {
private val placeRepository: PlaceRepository by lazy { PlaceRepositoryImpl.getInstance(this) }
val getLogsUseCase by lazy { GetLogsUseCase(placeRepository) }
val getPlacesUseCase by lazy { GetPlacesUseCase(placeRepository)}
val getAllPlacesUseCase by lazy { GetAllPlacesUseCase(placeRepository)}
val updateLogsUseCase by lazy { UpdateLogsUseCase(placeRepository) }
val updatePlacesUseCase by lazy { UpdatePlacesUseCase(placeRepository) }
val removeLogUseCase by lazy {RemoveLogUseCase(placeRepository)}
override fun onCreate() {
super.onCreate()
val key = getString(R.string.kakao_api_key)

KakaoMapSdk.init(this, key)
}






}
Loading