diff --git a/README.md b/README.md index 91dbd079..18986352 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ # android-map-location + +카카오 맵 클론 코딩 +카카오로컬 API 사용 + +## 기능 요구 사항 +- 저장된 검색어를 선택하면 해당 검색어의 검색 결과가 표시된다. +- 검색 결과 목록 중 하나의 항목을 선택하면 해당 항목의 위치를 지도에 표시한다. +- 앱 종료 시 마지막 위치를 저장하여 다시 앱 실행 시 해당 위치로 포커스 한다. +- 카카오지도 onMapError() 호출 시 에러 화면을 보여준다. +- +## 프로그래밍 요구 사항 +- BottomSheet를 사용한다. +- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다. +- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다. +- 코드 컨벤션을 준수하며 프로그래밍한다. + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 33c4ca53..4de1ea7b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" } @@ -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) \ No newline at end of file diff --git a/app/build.gradle.kts.rej b/app/build.gradle.kts.rej new file mode 100644 index 00000000..75749712 --- /dev/null +++ b/app/build.gradle.kts.rej @@ -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 diff --git a/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt new file mode 100644 index 00000000..55428797 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/MapActivityTest.kt @@ -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.presentation.MapActivity +import campus.tech.kakao.map.presentation.SearchActivity +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(SearchActivity::class.java.name)) + Intents.release() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/RepositoryImplTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/RepositoryImplTest.kt new file mode 100644 index 00000000..c707bb46 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/RepositoryImplTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt new file mode 100644 index 00000000..cc8ae613 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt @@ -0,0 +1,61 @@ +package campus.tech.kakao.map + +import android.content.Context +import android.content.SharedPreferences +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import campus.tech.kakao.map.presentation.MapActivity +import campus.tech.kakao.map.presentation.SearchActivity +import campus.tech.kakao.map.presentation.adapter.SearchedPlaceAdapter +import org.hamcrest.CoreMatchers.instanceOf +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 SearchActivityTest { + + private lateinit var sharedPreferences: SharedPreferences + private lateinit var context: Context + + @get:Rule + var activityScenarioRule = ActivityScenarioRule(SearchActivity::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)) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bca2f54..e38c414c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + @@ -21,6 +24,14 @@ + + + + - + \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/MainActivity.kt deleted file mode 100644 index 95b43803..00000000 --- a/app/src/main/java/campus/tech/kakao/map/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package campus.tech.kakao.map - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/campus/tech/kakao/map/PlaceApplication.kt b/app/src/main/java/campus/tech/kakao/map/PlaceApplication.kt new file mode 100644 index 00000000..1c7d3a1d --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/PlaceApplication.kt @@ -0,0 +1,39 @@ +package campus.tech.kakao.map + +import android.app.Application +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import campus.tech.kakao.map.data.PlaceRepositoryImpl +import campus.tech.kakao.map.domain.repository.PlaceRepository +import com.kakao.vectormap.KakaoMapSdk + +class PlaceApplication: Application() { + + val placeRepository: PlaceRepository by lazy { PlaceRepositoryImpl.getInstance(this)} + + override fun onCreate() { + super.onCreate() + appInstance = this + + val key = getString(R.string.kakao_api_key) + KakaoMapSdk.init(this, key) + } + companion object { + @Volatile + private lateinit var appInstance: PlaceApplication + fun isNetworkActive(): Boolean { + val connectivityManager: ConnectivityManager = + appInstance.getSystemService(ConnectivityManager::class.java) + val network: Network = connectivityManager.activeNetwork ?: return false + val actNetwork: NetworkCapabilities = + connectivityManager.getNetworkCapabilities(network) ?: return false + + return when { + actNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + actNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + else -> false + } + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceRepositoryImpl.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepositoryImpl.kt new file mode 100644 index 00000000..50315bd2 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceRepositoryImpl.kt @@ -0,0 +1,130 @@ +package campus.tech.kakao.map.data + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.PlaceApplication +import campus.tech.kakao.map.data.net.KakaoApiClient +import campus.tech.kakao.map.util.PlaceContract +import campus.tech.kakao.map.domain.model.Place +import campus.tech.kakao.map.domain.repository.PlaceRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PlaceRepositoryImpl(context: Context): + SQLiteOpenHelper(context, PlaceContract.DATABASE_NAME, null, 1), + PlaceRepository { + + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL(PlaceContract.CREATE_QUERY) + db?.execSQL(PlaceContract.CREATE_LOG_QUERY) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL(PlaceContract.DROP_QUERY) + db?.execSQL(PlaceContract.DROP_LOG_QUERY) + onCreate(db) + } + override suspend fun getPlaces(keyword: String): List = + withContext(Dispatchers.IO){ + val resultPlaces = mutableListOf() + for (page in 1..3) { + val response = KakaoApiClient.api.getSearchKeyword( + key = BuildConfig.KAKAO_REST_API_KEY, + query = keyword, + size = 15, + page = page + ) + if (response.isSuccessful) { + response.body()?.documents?.let { resultPlaces.addAll(it) } + } else throw RuntimeException("통신 에러 발생") + } + updatePlaces(resultPlaces) + resultPlaces + } + override suspend fun updatePlaces(places: List) { + val db = writableDatabase + + db.execSQL(PlaceContract.DELETE_QUERY) + places.forEach { + val values = ContentValues().apply { + put(PlaceContract.COLUMN_ID, it.id) + put(PlaceContract.COLUMN_NAME, it.place) + put(PlaceContract.COLUMN_LOCATION, it.address) + put(PlaceContract.COLUMN_TYPE, it.category) + put(PlaceContract.COLUMN_X_POS, it.xPos) + put(PlaceContract.COLUMN_Y_POS, it.yPos) + } + db.insert(PlaceContract.TABLE_NAME, null, values) + } + } + + override fun getPlaceById(id: String): Place? { + val cursor = readableDatabase.query( + PlaceContract.TABLE_NAME, + null, "${PlaceContract.COLUMN_ID} = ?", arrayOf(id), null, null, null + ) + var place: Place? = null + cursor?.use { + if (it.moveToFirst()) { + val name = it.getString(it.getColumnIndexOrThrow(PlaceContract.COLUMN_NAME)) + val address = it.getString(it.getColumnIndexOrThrow(PlaceContract.COLUMN_LOCATION)) + val type = it.getString(it.getColumnIndexOrThrow(PlaceContract.COLUMN_TYPE)) + val xPos = it.getString(it.getColumnIndexOrThrow(PlaceContract.COLUMN_X_POS)) + val yPos = it.getString(it.getColumnIndexOrThrow(PlaceContract.COLUMN_Y_POS)) + place = Place(id, name, address, type, xPos, yPos) + } + } + return place + } + + override fun updateLogs(logs: List) { + val db = writableDatabase + db.execSQL(PlaceContract.DELETE_LOG_QUERY) + logs.forEach { placeLog -> + val values = ContentValues().apply { + put(PlaceContract.COLUMN_LOG_ID, placeLog.id) + put(PlaceContract.COLUMN_LOG_NAME, placeLog.place) + } + db.insert(PlaceContract.TABLE_LOG_NAME, null, values) + } + } + + override fun removeLog(id: String) { + val db = writableDatabase + db.delete(PlaceContract.TABLE_LOG_NAME, "${PlaceContract.COLUMN_LOG_ID}=?", arrayOf(id)) + } + + override fun getLogs(): List { + val logs = mutableListOf() + val cursor = readableDatabase.query( + PlaceContract.TABLE_LOG_NAME, + null, null, null, null, null, null + ) + cursor?.use { + while (it.moveToNext()) { + val name = it.getString(it.getColumnIndexOrThrow(PlaceContract.COLUMN_LOG_NAME)) + val id = it.getString(it.getColumnIndexOrThrow(PlaceContract.COLUMN_LOG_ID)) + logs.add(Place(id,name, "", "", "","")) + } + } + return logs + } + + companion object { + + @Volatile + private var INSTANCE: PlaceRepositoryImpl? = null + + fun getInstance(context: Context): PlaceRepositoryImpl { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: PlaceRepositoryImpl(context.applicationContext).also { INSTANCE = it } + } + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/data/net/KakaoApi.kt b/app/src/main/java/campus/tech/kakao/map/data/net/KakaoApi.kt new file mode 100644 index 00000000..1a4264d7 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/net/KakaoApi.kt @@ -0,0 +1,18 @@ +package campus.tech.kakao.map.data.net + +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.domain.model.ResultSearchKeyword +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface KakaoApi { + @GET("v2/local/search/keyword.json") + suspend fun getSearchKeyword( + @Header("Authorization") key: String, + @Query("query") query: String, + @Query("size") size: Int = 15, + @Query("page") page: Int = 1 + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/data/net/KakaoApiClient.kt b/app/src/main/java/campus/tech/kakao/map/data/net/KakaoApiClient.kt new file mode 100644 index 00000000..e3f34c03 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/data/net/KakaoApiClient.kt @@ -0,0 +1,20 @@ +package campus.tech.kakao.map.data.net + + +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.domain.model.ResultSearchKeyword +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object KakaoApiClient { + private const val BASE_URL = "https://dapi.kakao.com/" + + val api: KakaoApi by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(KakaoApi::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/domain/model/Place.kt b/app/src/main/java/campus/tech/kakao/map/domain/model/Place.kt new file mode 100644 index 00000000..643e5f4a --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/model/Place.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map.domain.model + +import com.google.gson.annotations.SerializedName +import java.io.Serializable + + +data class Place( + @SerializedName("id") var id: String, + @SerializedName("place_name") var place: String, + @SerializedName("address_name") var address: String, + @SerializedName("category_name")var category: String, + @SerializedName("x") var xPos: String, + @SerializedName("y") var yPos: String +): Serializable diff --git a/app/src/main/java/campus/tech/kakao/map/domain/model/ResultSearchKeyword.kt b/app/src/main/java/campus/tech/kakao/map/domain/model/ResultSearchKeyword.kt new file mode 100644 index 00000000..607bd9d4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/model/ResultSearchKeyword.kt @@ -0,0 +1,15 @@ +package campus.tech.kakao.map.domain.model + +import com.google.gson.annotations.SerializedName + +data class ResultSearchKeyword( + var documents: List, + val meta: PlaceMeta +) +data class PlaceMeta ( + @SerializedName("total_count") var totalCount: Int, + @SerializedName("pageable_count") var pageableCount: Int, + @SerializedName("is_end") var isEnd: Boolean +) + + diff --git a/app/src/main/java/campus/tech/kakao/map/domain/repository/PlaceRepository.kt b/app/src/main/java/campus/tech/kakao/map/domain/repository/PlaceRepository.kt new file mode 100644 index 00000000..5b31d557 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/domain/repository/PlaceRepository.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.domain.repository + +import campus.tech.kakao.map.domain.model.Place + +interface PlaceRepository { + suspend fun getPlaces(placeName: String): List + suspend fun updatePlaces(places:List) + fun getPlaceById(id: String):Place? + fun getLogs(): List + fun updateLogs(placeLog: List) + fun removeLog(id: String) + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/presentation/MapActivity.kt new file mode 100644 index 00000000..02700068 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/MapActivity.kt @@ -0,0 +1,145 @@ +package campus.tech.kakao.map.presentation + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import campus.tech.kakao.map.PlaceApplication +import campus.tech.kakao.map.R +import campus.tech.kakao.map.domain.model.Place +import com.kakao.vectormap.KakaoMap +import com.kakao.vectormap.KakaoMapReadyCallback +import com.kakao.vectormap.LatLng +import com.kakao.vectormap.MapLifeCycleCallback +import com.kakao.vectormap.MapView +import com.kakao.vectormap.camera.CameraUpdateFactory +import com.kakao.vectormap.label.LabelOptions +import com.kakao.vectormap.label.LabelStyle +import com.kakao.vectormap.label.LabelStyles + +class MapActivity : AppCompatActivity() { + private val mapView by lazy { findViewById(R.id.mapView) } + private val searchView by lazy { findViewById(R.id.searchView) } + private lateinit var resultLauncher: ActivityResultLauncher + private lateinit var mapBottomSheet: MapBottomSheet + private lateinit var tvErrorMessage: TextView + private lateinit var kakaoMap: KakaoMap + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_map) + + initializeMapView() + initializeSearchView() + setResultLauncher() + } + + private fun initializeMapView() { + mapView.start(object : MapLifeCycleCallback() { + override fun onMapDestroy() {} + override fun onMapError(error: Exception) { + showErrorPage(error) + } + }, object : KakaoMapReadyCallback() { + override fun onMapReady(map: KakaoMap) { + if(!isNetworkAvailable()){ + showErrorPage(java.lang.Exception("네트워크 연결 오류")) + } + kakaoMap = map + initMap() + } + }) + } + + private fun isNetworkAvailable(): Boolean { + return PlaceApplication.isNetworkActive() + } + + private fun initializeSearchView() { + searchView.setOnClickListener { + val intent = Intent(this, SearchActivity::class.java) + resultLauncher.launch(intent) + } + } + + private fun showErrorPage(error: Exception){ + setContentView(R.layout.error_page) + tvErrorMessage = findViewById(R.id.tvErrorMessage) + tvErrorMessage.text = "지도 인증에 실패했습니다.\n다시 시도해주세요.\n"+ error.message + } + + private fun setResultLauncher() { + resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val placeData = result.data?.getSerializableExtra("placeData") as? Place + placeData?.let { + updateMapWithPlaceData(it) + saveLastVisitedPlace(it) + showBottomSheet(it) + } + } + } + } + + private fun updateMapWithPlaceData(place: Place) { + val cameraUpdate = CameraUpdateFactory.newCenterPosition( + LatLng.from(place.yPos.toDouble(), place.xPos.toDouble()), 15 + ) + kakaoMap.moveCamera(cameraUpdate) + + val styles = kakaoMap.labelManager?.addLabelStyles( + LabelStyles.from(LabelStyle.from(R.drawable.icon_location3)) + ) + val options = LabelOptions.from( + LatLng.from(place.yPos.toDouble(), place.xPos.toDouble()) + ).setStyles(styles) + + val layer = kakaoMap.labelManager?.layer + layer?.addLabel(options) + } + + private fun showBottomSheet(place: Place) { + mapBottomSheet = MapBottomSheet(place) + mapBottomSheet.show(supportFragmentManager, mapBottomSheet.tag) + } + + private fun initMap() { + val sharedPreferences = getSharedPreferences("LastVisitedPlace", MODE_PRIVATE) + val placeName = sharedPreferences.getString("placeName", null) + val roadAddressName = sharedPreferences.getString("roadAddressName", null) + val categoryName = sharedPreferences.getString("categoryName", null) + val yPos = sharedPreferences.getString("yPos", null) + val xPos = sharedPreferences.getString("xPos", null) + + if (placeName != null && roadAddressName != null && categoryName != null && yPos != null && xPos != null) { + val place = Place("", placeName, roadAddressName, categoryName, xPos, yPos) + updateMapWithPlaceData(place) + showBottomSheet(place) + } + } + + private fun saveLastVisitedPlace(place: Place) { + val sharedPreferences = getSharedPreferences("LastVisitedPlace", MODE_PRIVATE) + val editor = sharedPreferences.edit() + editor.putString("placeName", place.place) + editor.putString("roadAddressName", place.address) + editor.putString("categoryName", place.category) + editor.putString("yPos", place.yPos) + editor.putString("xPos", place.xPos) + editor.apply() + } + + override fun onResume() { + super.onResume() + mapView.resume() + } + + override fun onPause() { + super.onPause() + mapView.pause() + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/MapBottomSheet.kt b/app/src/main/java/campus/tech/kakao/map/presentation/MapBottomSheet.kt new file mode 100644 index 00000000..8eeeb3f3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/MapBottomSheet.kt @@ -0,0 +1,27 @@ +package campus.tech.kakao.map.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.domain.model.Place +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class MapBottomSheet(private val place: Place) : BottomSheetDialogFragment() { + private lateinit var tvPlaceName : TextView + private lateinit var tvPlaceAddress: TextView + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + val view = inflater.inflate(R.layout.bottom_sheet, container, false) + tvPlaceName = view.findViewById(R.id.tvPlaceName) + tvPlaceAddress = view.findViewById(R.id.tvPlaceAddress) + tvPlaceName.text = place.place + tvPlaceAddress.text = place.place + return view + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/presentation/SearchActivity.kt new file mode 100644 index 00000000..868de2fa --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/SearchActivity.kt @@ -0,0 +1,105 @@ +package campus.tech.kakao.map.presentation + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivityMainBinding +import campus.tech.kakao.map.presentation.adapter.SearchedPlaceAdapter +import campus.tech.kakao.map.presentation.adapter.LogAdapter +import campus.tech.kakao.map.domain.model.Place +import campus.tech.kakao.map.util.PlaceMapper +import kotlinx.coroutines.launch + +class SearchActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private lateinit var searchedPlaceAdapter: SearchedPlaceAdapter + private lateinit var logAdapter: LogAdapter + private lateinit var viewModel: SearchViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + init() + } + + private fun init() { + initViewModel() + initBinding() + setupRecyclerViews() + observeViewModel() + } + + private fun initViewModel() { + viewModel = ViewModelProvider(this,SearchViewModel.Factory) + .get(SearchViewModel::class.java) + } + + private fun initBinding() { + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + binding.lifecycleOwner = this + binding.viewModel = viewModel + } + + private fun setupRecyclerViews() { + setupSearchedPlaceRecyclerView() + setupLogRecyclerView() + } + + private fun setupSearchedPlaceRecyclerView() { + val searchedPlaceRecyclerView = binding.recyclerPlace + searchedPlaceAdapter = SearchedPlaceAdapter { place -> + viewModel.updateLogs(place) + handlePlaceClick(place) + } + + searchedPlaceRecyclerView.apply { + layoutManager = LinearLayoutManager(this@SearchActivity) + addItemDecoration(DividerItemDecoration(this@SearchActivity, DividerItemDecoration.VERTICAL )) + adapter = searchedPlaceAdapter + } + } + + private fun handlePlaceClick(place: Place) { + val intent = Intent(this, MapActivity::class.java).apply { + putExtra("placeData", viewModel.getPlaceById(place.id)) + } + setResult(RESULT_OK,intent) + finish() + } + + private fun setupLogRecyclerView() { + val logRecyclerView = binding.recyclerLog + logAdapter = LogAdapter { id -> viewModel.removeLog(id) } + logAdapter.submitList(viewModel.getLogs()) + + logRecyclerView.apply { + layoutManager = LinearLayoutManager(this@SearchActivity, RecyclerView.HORIZONTAL, false) + adapter = logAdapter + } + } + + private fun observeViewModel() { + + lifecycleScope.launch { + viewModel.searchedPlaces.collect { places -> + updateSearchedPlaceList(places) + binding.tvHelpMessage.visibility = if (places.isEmpty()) View.VISIBLE else View.GONE + } + } + viewModel.logList.observe(this, Observer { logList -> + logAdapter.submitList(PlaceMapper.mapPlaces(logList)) + }) + } + + private fun updateSearchedPlaceList(places: List) { + searchedPlaceAdapter.submitList(PlaceMapper.mapPlaces(places)) + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/SearchUiState.kt b/app/src/main/java/campus/tech/kakao/map/presentation/SearchUiState.kt new file mode 100644 index 00000000..ed05bfc5 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/SearchUiState.kt @@ -0,0 +1,10 @@ +package campus.tech.kakao.map.presentation + +import campus.tech.kakao.map.domain.model.Place + +data class SearchUiState( + val isloading: Boolean = false, + val isError: Boolean = false, + val Places: List = emptyList() + +) diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/presentation/SearchViewModel.kt new file mode 100644 index 00000000..1d1dbbf7 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/SearchViewModel.kt @@ -0,0 +1,90 @@ +package campus.tech.kakao.map.presentation + +import androidx.lifecycle.* +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import campus.tech.kakao.map.PlaceApplication +import campus.tech.kakao.map.domain.model.Place +import campus.tech.kakao.map.domain.repository.PlaceRepository +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn + +class SearchViewModel(private val repository: PlaceRepository) : ViewModel() { + + val searchText = MutableLiveData() + + private val _uiState = MutableStateFlow(SearchUiState(true,false)) + val UiState: StateFlow = _uiState.asStateFlow() + + private val _logList = MutableLiveData>() + val logList: LiveData> get() = _logList + + private val _searchedPlaces = searchText.asFlow() + .debounce(500L) + .flatMapLatest { query -> + if (query.isNotBlank()) { + flow { + val places = getPlaces(query) + emit(places) + } + } else { + flowOf(emptyList()) + } + }.stateIn(viewModelScope,SharingStarted.Lazily, emptyList()) + val searchedPlaces: StateFlow> get() = _searchedPlaces + + + init { + _logList.value = getLogs() + } + + fun clearSearch() { + searchText.value = "" + } + suspend fun getPlaces(keyword: String): List{ + return withContext(Dispatchers.IO) { repository.getPlaces(keyword) } + } + + fun getPlaceById(id: String): Place?{ + return repository.getPlaceById(id) + } + fun getLogs(): List { + return repository.getLogs() + } + + fun updateLogs(place: Place) { + val updatedList = _logList.value?.toMutableList() ?: mutableListOf() + val existingLog = updatedList.find { it.id == place.id } + if (existingLog != null) { + updatedList.remove(existingLog) + updatedList.add(0, existingLog) + } else { + updatedList.add(0, place) + } + _logList.value = updatedList + repository.updateLogs(updatedList) + } + + fun removeLog(id: String) { + repository.removeLog(id) + _logList.value = getLogs() + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val placeRepository = (this[APPLICATION_KEY] as PlaceApplication).placeRepository + SearchViewModel(repository = placeRepository) + } + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/adapter/LogAdapter.kt b/app/src/main/java/campus/tech/kakao/map/presentation/adapter/LogAdapter.kt new file mode 100644 index 00000000..d972bfef --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/adapter/LogAdapter.kt @@ -0,0 +1,33 @@ +package campus.tech.kakao.map.presentation.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.util.DiffUtilCallback +import campus.tech.kakao.map.databinding.LogItemBinding +import campus.tech.kakao.map.domain.model.Place + +class LogAdapter( + private val onRemoveLog: (String) -> Unit +) + : ListAdapter(DiffUtilCallback()) { + inner class LogViewHolder(private val binding: LogItemBinding) + : RecyclerView.ViewHolder(binding.root){ + fun bind(place: Place){ + binding.place = place + binding.btnLogDel.setOnClickListener { + onRemoveLog(place.id) + } + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LogViewHolder { + val binding = LogItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) + return LogViewHolder(binding) + } + + override fun onBindViewHolder(holder: LogViewHolder, position: Int) { + val location = getItem(position) + holder.bind(location) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/adapter/SearchedPlaceAdapter.kt b/app/src/main/java/campus/tech/kakao/map/presentation/adapter/SearchedPlaceAdapter.kt new file mode 100644 index 00000000..1d49c77f --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/presentation/adapter/SearchedPlaceAdapter.kt @@ -0,0 +1,34 @@ +package campus.tech.kakao.map.presentation.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.util.DiffUtilCallback +import campus.tech.kakao.map.domain.model.Place +import campus.tech.kakao.map.databinding.ListItemBinding + + +class SearchedPlaceAdapter( + private var onItemClicked: (Place) -> Unit +): ListAdapter(DiffUtilCallback()) { + + inner class LocationViewHolder(private val binding: ListItemBinding ) + :RecyclerView.ViewHolder(binding.root){ + fun bind(place: Place){ + binding.place = place + binding.root.setOnClickListener { + onItemClicked(place) + } + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocationViewHolder { + val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) + return LocationViewHolder(binding) + } + + override fun onBindViewHolder(holder: LocationViewHolder, position: Int) { + val location = getItem(position) + holder.bind(location) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/util/DiffUtilCalback.kt b/app/src/main/java/campus/tech/kakao/map/util/DiffUtilCalback.kt new file mode 100644 index 00000000..7694ea34 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/util/DiffUtilCalback.kt @@ -0,0 +1,14 @@ +package campus.tech.kakao.map.util + +import androidx.recyclerview.widget.DiffUtil +import campus.tech.kakao.map.domain.model.Place + +class DiffUtilCallback: DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: Place, newItem: Place): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Place, newItem: Place): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/util/PlaceContract.kt b/app/src/main/java/campus/tech/kakao/map/util/PlaceContract.kt new file mode 100644 index 00000000..34647795 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/util/PlaceContract.kt @@ -0,0 +1,37 @@ +package campus.tech.kakao.map.util + +object PlaceContract { + const val DATABASE_NAME = "place.db" + + const val TABLE_NAME: String = "db_place" + const val COLUMN_ID: String = "id" + const val COLUMN_NAME: String = "name" + const val COLUMN_LOCATION: String = "place" + const val COLUMN_TYPE: String = "type" + const val COLUMN_X_POS: String = "x_pos" + const val COLUMN_Y_POS: String = "y_pos" + + const val CREATE_QUERY = "CREATE TABLE $TABLE_NAME (" + + "$COLUMN_ID TEXT NOT NULL, " + + "$COLUMN_NAME TEXT NOT NULL, " + + "$COLUMN_LOCATION TEXT NOT NULL, " + + "$COLUMN_TYPE TEXT, " + + "$COLUMN_X_POS TEXT NOT NULL, " + + "$COLUMN_Y_POS TEXT NOT NULL " + + ");" + + const val DELETE_QUERY = "DELETE FROM $TABLE_NAME" + const val DROP_QUERY = "DROP TABLE IF EXISTS $TABLE_NAME" + + const val TABLE_LOG_NAME = "db_Log" + const val COLUMN_LOG_ID = "log_id" + const val COLUMN_LOG_NAME = "log_name" + + const val CREATE_LOG_QUERY = "CREATE TABLE IF NOT EXISTS $TABLE_LOG_NAME (" + + "$COLUMN_LOG_ID TEXT NOT NULL, " + + "$COLUMN_LOG_NAME TEXT NOT NULL" + + ");" + + const val DELETE_LOG_QUERY = "DELETE FROM $TABLE_LOG_NAME" + const val DROP_LOG_QUERY = "DROP TABLE IF EXISTS $TABLE_LOG_NAME" +} diff --git a/app/src/main/java/campus/tech/kakao/map/util/PlaceMapper.kt b/app/src/main/java/campus/tech/kakao/map/util/PlaceMapper.kt new file mode 100644 index 00000000..77d8f211 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/util/PlaceMapper.kt @@ -0,0 +1,21 @@ +package campus.tech.kakao.map.util + +import campus.tech.kakao.map.domain.model.Place + +class PlaceMapper { + companion object{ + fun mapPlaces(places: List): List { + return places.map { place -> + place.copy(category = setCategoryName(place.category), + place = setPlaceName(place.place) + ) + } + } + private fun setPlaceName(placeName: String): String { + return if (placeName.length > 12){ placeName.take(10)+"..." } else placeName + } + private fun setCategoryName(categoryName: String): String { + return categoryName.split(" ").lastOrNull() ?: "" + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_cancel.xml b/app/src/main/res/drawable/icon_cancel.xml new file mode 100644 index 00000000..eae1d2b5 --- /dev/null +++ b/app/src/main/res/drawable/icon_cancel.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/icon_location.png b/app/src/main/res/drawable/icon_location.png new file mode 100644 index 00000000..b3782a34 Binary files /dev/null and b/app/src/main/res/drawable/icon_location.png differ diff --git a/app/src/main/res/drawable/icon_location3.png b/app/src/main/res/drawable/icon_location3.png new file mode 100644 index 00000000..365df3ae Binary files /dev/null and b/app/src/main/res/drawable/icon_location3.png differ diff --git a/app/src/main/res/drawable/icon_location_resize.xml b/app/src/main/res/drawable/icon_location_resize.xml new file mode 100644 index 00000000..f8c25aa8 --- /dev/null +++ b/app/src/main/res/drawable/icon_location_resize.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_search.xml b/app/src/main/res/drawable/icon_search.xml new file mode 100644 index 00000000..4b45fe81 --- /dev/null +++ b/app/src/main/res/drawable/icon_search.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_search2.xml b/app/src/main/res/drawable/icon_search2.xml new file mode 100644 index 00000000..bd1c6e6e --- /dev/null +++ b/app/src/main/res/drawable/icon_search2.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_x.png b/app/src/main/res/drawable/icon_x.png new file mode 100644 index 00000000..0f88813d Binary files /dev/null and b/app/src/main/res/drawable/icon_x.png differ diff --git a/app/src/main/res/drawable/searchview_background.xml b/app/src/main/res/drawable/searchview_background.xml new file mode 100644 index 00000000..2e1ea812 --- /dev/null +++ b/app/src/main/res/drawable/searchview_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/searchview_background2.xml b/app/src/main/res/drawable/searchview_background2.xml new file mode 100644 index 00000000..bd5a8698 --- /dev/null +++ b/app/src/main/res/drawable/searchview_background2.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..cc37bc75 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,104 @@ - - - - - + xmlns:tools="http://schemas.android.com/tools"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml new file mode 100644 index 00000000..4aa3fc52 --- /dev/null +++ b/app/src/main/res/layout/activity_map.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/bottom_sheet.xml b/app/src/main/res/layout/bottom_sheet.xml new file mode 100644 index 00000000..d3c7b78b --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/layout/error_page.xml b/app/src/main/res/layout/error_page.xml new file mode 100644 index 00000000..d93520cc --- /dev/null +++ b/app/src/main/res/layout/error_page.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item.xml b/app/src/main/res/layout/list_item.xml new file mode 100644 index 00000000..86b8ce55 --- /dev/null +++ b/app/src/main/res/layout/list_item.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/log_item.xml b/app/src/main/res/layout/log_item.xml new file mode 100644 index 00000000..1615af83 --- /dev/null +++ b/app/src/main/res/layout/log_item.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 05ed4b9e..b9ad7b3f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,6 +2,7 @@ diff --git a/settings.gradle.kts b/settings.gradle.kts index ff9d5255..60eb0001 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") + maven("https://devrepo.kakao.com/nexus/content/groups/public/") } }