diff --git a/.gitignore b/.gitignore
index b4959436..d658d9f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,3 @@
-.DS_Store
-### Android template
# Gradle files
.gradle/
build/
@@ -33,5 +31,6 @@ google-services.json
# Android Profiling
*.hprof
-/keyStore
-/app/release
+
+# Mac OS
+.DS_Store
diff --git a/README.md b/README.md
index 73ba54e1..18986352 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,18 @@
-# android-map-refactoring
+# android-map-location
+
+카카오 맵 클론 코딩
+카카오로컬 API 사용
+
+## 기능 요구 사항
+- 저장된 검색어를 선택하면 해당 검색어의 검색 결과가 표시된다.
+- 검색 결과 목록 중 하나의 항목을 선택하면 해당 항목의 위치를 지도에 표시한다.
+- 앱 종료 시 마지막 위치를 저장하여 다시 앱 실행 시 해당 위치로 포커스 한다.
+- 카카오지도 onMapError() 호출 시 에러 화면을 보여준다.
+-
+## 프로그래밍 요구 사항
+- BottomSheet를 사용한다.
+- 카카오 API 사용을 위한 앱 키를 외부에 노출하지 않는다.
+- 가능한 MVVM 아키텍처 패턴을 적용하도록 한다.
+- 코드 컨벤션을 준수하며 프로그래밍한다.
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2c1a15f2..e3447cdf 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,22 +1,29 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
- id("org.jlleitschuh.gradle.ktlint")
- id("kotlin-parcelize")
- id("kotlin-kapt")
id("com.google.dagger.hilt.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"
}
@@ -39,42 +46,43 @@ android {
}
buildFeatures {
+ viewBinding = true
dataBinding = true
buildConfig = true
}
}
dependencies {
-
- implementation("androidx.core:core-ktx:1.13.1")
- implementation("androidx.appcompat:appcompat:1.7.0")
- implementation("com.google.android.material:material:1.12.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.activity:activity-ktx:1.9.0")
- implementation("androidx.test:core-ktx:1.6.1")
- implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
- implementation("androidx.room:room-runtime:2.6.1")
+ kapt("com.google.dagger:hilt-compiler:2.48.1")
kapt("androidx.room:room-compiler:2.6.1")
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+ implementation ("androidx.activity:activity-ktx:1.1.0")
+ implementation ("androidx.fragment:fragment-ktx:1.2.5")
implementation("com.google.dagger:hilt-android:2.48.1")
- kapt("com.google.dagger:hilt-compiler:2.48.1")
- implementation("androidx.activity:activity-ktx:1.9.0")
implementation("androidx.room:room-ktx:2.6.1")
- testImplementation("androidx.room:room-testing:2.6.1")
+ implementation("androidx.room:room-runtime:2.6.1")
+ 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("androidx.datastore:datastore-preferences:1.0.0")
+ implementation("androidx.activity:activity:1.8.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")
- testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
- androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
+ testImplementation("io.mockk:mockk:1.13.12")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1")
- androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1")
- kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6bca2f54..04e70f2a 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..3cbf6189
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/PlaceApplication.kt
@@ -0,0 +1,40 @@
+package campus.tech.kakao.map
+
+import android.app.Application
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.view.View
+import com.kakao.vectormap.KakaoMapSdk
+import dagger.hilt.android.HiltAndroidApp
+
+
+@HiltAndroidApp
+class PlaceApplication: Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ appInstance = this
+
+ initKakaoMapSdk()
+ }
+
+ private fun initKakaoMapSdk(){
+ 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 actNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
+ actNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ }
+ }
+}
diff --git a/app/src/main/java/campus/tech/kakao/map/data/LastVisitedPlaceManager.kt b/app/src/main/java/campus/tech/kakao/map/data/LastVisitedPlaceManager.kt
new file mode 100644
index 00000000..96b3832c
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/data/LastVisitedPlaceManager.kt
@@ -0,0 +1,34 @@
+package campus.tech.kakao.map.data
+
+import android.content.Context
+import campus.tech.kakao.map.domain.model.Place
+import javax.inject.Inject
+
+class LastVisitedPlaceManager @Inject constructor(context: Context) {
+
+ private val sharedPreferences = context.getSharedPreferences("LastVisitedPlace", Context.MODE_PRIVATE)
+
+ fun saveLastVisitedPlace(place: Place) {
+ 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()
+ }
+
+ fun getLastVisitedPlace(): Place? {
+ 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)
+
+ return if (placeName != null && roadAddressName != null && categoryName != null && yPos != null && xPos != null) {
+ Place("", placeName, roadAddressName, categoryName, xPos, yPos)
+ } else {
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceLocalDataRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceLocalDataRepository.kt
new file mode 100644
index 00000000..417526a6
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceLocalDataRepository.kt
@@ -0,0 +1,42 @@
+package campus.tech.kakao.map.data
+
+import campus.tech.kakao.map.data.dao.PlaceDao
+import campus.tech.kakao.map.data.entity.PlaceEntity
+import campus.tech.kakao.map.data.entity.PlaceLogEntity
+import campus.tech.kakao.map.domain.model.Place
+import campus.tech.kakao.map.domain.repository.PlaceRepository
+import javax.inject.Inject
+
+open class PlaceLocalDataRepository @Inject constructor(
+ private val placeDao: PlaceDao,
+) : PlaceRepository {
+
+ override suspend fun getPlaces(placeName: String): List {
+ return placeDao.getPlaces(placeName).map { it.toPlace() }
+ }
+
+ override suspend fun updatePlaces(places: List) {
+ placeDao.deleteAllPlaces()
+ placeDao.insertPlaces(places.map {
+ PlaceEntity(it.id, it.place, it.address, it.category, it.xPos, it.yPos)
+ })
+ }
+
+ override suspend fun getPlaceById(id: String): Place? {
+ return placeDao.getPlaceById(id)?.toPlace()
+ }
+
+ override suspend fun updateLogs(logs: List) {
+ placeDao.deleteAllLogs()
+ placeDao.insertLogs(logs.map { PlaceLogEntity(it.id, it.place) })
+ }
+
+ override suspend fun removeLog(id: String) {
+ placeDao.removeLog(id)
+
+ }
+
+ override suspend fun getLogs(): List {
+ return placeDao.getLogs().map { it.toPlace() }
+ }
+}
diff --git a/app/src/main/java/campus/tech/kakao/map/data/PlaceRemoteDataRepository.kt b/app/src/main/java/campus/tech/kakao/map/data/PlaceRemoteDataRepository.kt
new file mode 100644
index 00000000..b5ef6485
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/data/PlaceRemoteDataRepository.kt
@@ -0,0 +1,35 @@
+package campus.tech.kakao.map.data
+
+import android.content.Context
+import campus.tech.kakao.map.BuildConfig
+import campus.tech.kakao.map.data.dao.PlaceDao
+import campus.tech.kakao.map.data.net.KakaoApi
+import campus.tech.kakao.map.domain.model.Place
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class PlaceRemoteDataRepository @Inject constructor(
+ private val placeDao: PlaceDao,
+ private val kakaoApi: KakaoApi
+) : PlaceLocalDataRepository(placeDao){
+ override suspend fun getPlaces(placeName: String): List {
+ return withContext(Dispatchers.IO) {
+ val resultPlaces = mutableListOf()
+ for (page in 1..3) {
+ val response = kakaoApi.getSearchKeyword(
+ key = BuildConfig.KAKAO_REST_API_KEY,
+ query = placeName,
+ size = 15,
+ page = page
+ )
+ if (response.isSuccessful) {
+ response.body()?.documents?.let { resultPlaces.addAll(it) }
+ } else throw RuntimeException("통신 에러 발생")
+ }
+ updatePlaces(resultPlaces)
+ resultPlaces
+ }
+ }
+}
diff --git a/app/src/main/java/campus/tech/kakao/map/data/dao/PlaceDao.kt b/app/src/main/java/campus/tech/kakao/map/data/dao/PlaceDao.kt
new file mode 100644
index 00000000..26821b78
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/data/dao/PlaceDao.kt
@@ -0,0 +1,34 @@
+package campus.tech.kakao.map.data.dao
+
+import androidx.room.*
+import androidx.room.Insert
+import androidx.room.Query
+import campus.tech.kakao.map.data.entity.PlaceEntity
+import campus.tech.kakao.map.data.entity.PlaceLogEntity
+
+@Dao
+interface PlaceDao {
+ @Query("SELECT * FROM places WHERE place LIKE :keyword")
+ suspend fun getPlaces(keyword: String): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertPlaces(places: List)
+
+ @Query("SELECT * FROM places WHERE id = :id")
+ suspend fun getPlaceById(id: String): PlaceEntity?
+
+ @Query("DELETE FROM places")
+ suspend fun deleteAllPlaces()
+
+ @Query("SELECT * FROM logs")
+ suspend fun getLogs(): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertLogs(logs: List)
+
+ @Query("DELETE FROM logs WHERE id = :id")
+ suspend fun removeLog(id: String)
+
+ @Query("DELETE FROM logs")
+ suspend fun deleteAllLogs()
+}
\ No newline at end of file
diff --git a/app/src/main/java/campus/tech/kakao/map/data/database/PlaceDatabase.kt b/app/src/main/java/campus/tech/kakao/map/data/database/PlaceDatabase.kt
new file mode 100644
index 00000000..5b0d9a28
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/data/database/PlaceDatabase.kt
@@ -0,0 +1,11 @@
+package campus.tech.kakao.map.data.database
+
+import androidx.room.*
+import campus.tech.kakao.map.data.dao.PlaceDao
+import campus.tech.kakao.map.data.entity.PlaceEntity
+import campus.tech.kakao.map.data.entity.PlaceLogEntity
+
+@Database(entities = [PlaceEntity::class, PlaceLogEntity::class], version = 1)
+abstract class PlaceDatabase : RoomDatabase() {
+ abstract fun placeDao(): PlaceDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/campus/tech/kakao/map/data/entity/Entity.kt b/app/src/main/java/campus/tech/kakao/map/data/entity/Entity.kt
new file mode 100644
index 00000000..e191bad6
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/data/entity/Entity.kt
@@ -0,0 +1,25 @@
+package campus.tech.kakao.map.data.entity
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import campus.tech.kakao.map.domain.model.Place
+
+@Entity(tableName = "places")
+data class PlaceEntity(
+ @PrimaryKey val id: String,
+ val place: String,
+ val address: String,
+ val type: String,
+ val xPos: String,
+ val yPos: String
+) {
+ fun toPlace() = Place(id, place, address, type, xPos, yPos)
+}
+
+@Entity(tableName = "logs")
+data class PlaceLogEntity(
+ @PrimaryKey val id: String,
+ val place: String
+) {
+ fun toPlace() = Place(id, place, "", "", "", "")
+}
\ No newline at end of file
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/di/DatabaseModule.kt b/app/src/main/java/campus/tech/kakao/map/di/DatabaseModule.kt
new file mode 100644
index 00000000..67f817b8
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/di/DatabaseModule.kt
@@ -0,0 +1,33 @@
+package campus.tech.kakao.map.di
+
+import android.content.Context
+import androidx.room.Room
+import campus.tech.kakao.map.data.dao.PlaceDao
+import campus.tech.kakao.map.data.database.PlaceDatabase
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DatabaseModule {
+
+ @Provides
+ @Singleton
+ fun provideAppDatabase(@ApplicationContext context: Context): PlaceDatabase {
+ return Room.databaseBuilder(
+ context.applicationContext,
+ PlaceDatabase::class.java,
+ "place_database"
+ ).build()
+ }
+
+ @Provides
+ @Singleton
+ fun providePlaceDao(placeDatabase: PlaceDatabase): PlaceDao {
+ return placeDatabase.placeDao()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt b/app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt
new file mode 100644
index 00000000..8857152c
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/di/NetworkModule.kt
@@ -0,0 +1,30 @@
+package campus.tech.kakao.map.di
+
+import campus.tech.kakao.map.data.net.KakaoApi
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NetworkModule {
+ private const val BASE_URL = "https://dapi.kakao.com/"
+ @Singleton
+ @Provides
+ fun provideRetrofit(): Retrofit {
+ return Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ }
+
+ @Singleton
+ @Provides
+ fun provideKakaoApi(retrofit: Retrofit): KakaoApi {
+ return retrofit.create(KakaoApi::class.java)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/campus/tech/kakao/map/di/ViewModelModule.kt b/app/src/main/java/campus/tech/kakao/map/di/ViewModelModule.kt
new file mode 100644
index 00000000..d8eec735
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/di/ViewModelModule.kt
@@ -0,0 +1,37 @@
+package campus.tech.kakao.map.di
+
+import android.content.Context
+import campus.tech.kakao.map.PlaceApplication
+import campus.tech.kakao.map.data.*
+import campus.tech.kakao.map.data.dao.PlaceDao
+import campus.tech.kakao.map.data.net.KakaoApi
+import campus.tech.kakao.map.domain.repository.PlaceRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object ViewModelModule {
+ // SearchViewModel
+ @Provides
+ fun providePlaceRepository(
+ placeDao: PlaceDao, kakaoApi: KakaoApi): PlaceRepository {
+ return if (PlaceApplication.isNetworkActive()) {
+ PlaceRemoteDataRepository(placeDao,kakaoApi)
+ } else {
+ PlaceLocalDataRepository(placeDao)
+ }
+ }
+
+ // MapViewModel
+ @Provides
+ fun provideLastVisitedPlaceManager(@ApplicationContext context: Context): LastVisitedPlaceManager{
+ return LastVisitedPlaceManager(context)
+ }
+}
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..8fadfa99
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/domain/model/ResultSearchKeyword.kt
@@ -0,0 +1,10 @@
+package campus.tech.kakao.map.domain.model
+
+import com.google.gson.annotations.SerializedName
+
+data class ResultSearchKeyword(
+ var documents: List
+)
+
+
+
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..d5426c30
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/domain/repository/PlaceRepository.kt
@@ -0,0 +1,12 @@
+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)
+ suspend fun getPlaceById(id: String):Place?
+ suspend fun getLogs(): List
+ suspend fun updateLogs(logs: List)
+ suspend fun removeLog(id: String)
+}
\ No newline at end of file
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/presentation/map/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/presentation/map/MapActivity.kt
new file mode 100644
index 00000000..57d65e7a
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/presentation/map/MapActivity.kt
@@ -0,0 +1,187 @@
+package campus.tech.kakao.map.presentation.map
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsetsController
+import android.widget.TextView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.lifecycleScope
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import campus.tech.kakao.map.PlaceApplication
+import campus.tech.kakao.map.R
+import campus.tech.kakao.map.databinding.ActivityMapBinding
+import campus.tech.kakao.map.domain.model.Place
+import campus.tech.kakao.map.presentation.search.SearchActivity
+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
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class MapActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMapBinding
+ private lateinit var resultLauncher: ActivityResultLauncher
+ private lateinit var mapBottomSheet: MapBottomSheet
+ private lateinit var kakaoMap: KakaoMap
+ private val mapViewModel: MapViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setStatusBarTransparent()
+ initBinding()
+ collectViewModel()
+ initSwipeRefreshLayout()
+ initMapView()
+ initSearchView()
+ setResultLauncher()
+ }
+
+ private fun setStatusBarTransparent() {
+ this.window?.apply {
+ this.statusBarColor = Color.TRANSPARENT
+ decorView.systemUiVisibility =
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ }
+ }
+
+ private fun initBinding(){
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_map)
+ binding.lifecycleOwner = this
+ binding.viewModel = mapViewModel
+ }
+
+ private fun collectViewModel() {
+ lifecycleScope.launch {
+ mapViewModel.lastVisitedPlace.collect { place ->
+ place?.let {
+ updateMapWithPlaceData(it)
+ showBottomSheet(it)
+ }
+ }
+ }
+ }
+ private fun initSwipeRefreshLayout() {
+ binding.swipeRefreshLayout.setOnRefreshListener {
+ if (!isNetworkAvailable()) {
+ showErrorPage(Exception("네트워크 연결 오류"))
+ }else{
+ showMapPage()
+ showBottomSheet(mapViewModel.lastVisitedPlace.value)
+ binding.swipeRefreshLayout.isEnabled = false
+ }
+ binding.swipeRefreshLayout.isRefreshing = false
+ }
+ }
+ private fun initMapView() {
+ binding.mapView.start(object : MapLifeCycleCallback() {
+ override fun onMapDestroy() {}
+ override fun onMapError(error: Exception) {
+ showErrorPage(error)
+ }
+ }, object : KakaoMapReadyCallback() {
+ override fun onMapReady(map: KakaoMap) {
+ kakaoMap = map
+ if (!isNetworkAvailable()) {
+ showErrorPage(Exception("네트워크 연결 오류"))
+ }else{
+ binding.swipeRefreshLayout.isEnabled = false
+ initMapPage()
+ }
+ }
+ })
+ }
+
+ private fun isNetworkAvailable(): Boolean {
+ return PlaceApplication.isNetworkActive()
+ }
+
+ private fun initMapPage(){
+ showMapPage()
+ mapViewModel.loadLastVisitedPlace()
+ }
+
+ private fun showMapPage(){
+ binding.tvErrorMessage.visibility = View.GONE
+ binding.searchView.visibility = View.VISIBLE
+ binding.mapView.visibility = View.VISIBLE
+ }
+
+ private fun showErrorPage(error: Exception) {
+ binding.tvErrorMessage.visibility = View.VISIBLE
+ binding.mapView.visibility = View.GONE
+ binding.searchView.visibility = View.GONE
+ binding.tvErrorMessage.text = "지도 인증에 실패했습니다.\n다시 시도해주세요.\n" + error.message
+ }
+
+ private fun initSearchView() {
+ binding.searchView.setOnClickListener {
+ val intent = Intent(this, SearchActivity::class.java)
+ resultLauncher.launch(intent)
+ }
+ }
+
+ private fun setResultLauncher() {
+ resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val placeData = result.data?.getSerializableExtra("placeData") as? Place
+ placeData?.let {
+ mapViewModel.saveLastVisitedPlace(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?) {
+ val bottomSheet = MapBottomSheet()
+ place?.let {
+ val args = Bundle()
+ args.putSerializable("place", it)
+ bottomSheet.arguments = args }
+ bottomSheet.show(supportFragmentManager, bottomSheet.tag)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ binding.mapView.resume()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ binding.mapView.pause()
+ }
+}
diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/map/MapBottomSheet.kt b/app/src/main/java/campus/tech/kakao/map/presentation/map/MapBottomSheet.kt
new file mode 100644
index 00000000..7f1acf17
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/presentation/map/MapBottomSheet.kt
@@ -0,0 +1,32 @@
+package campus.tech.kakao.map.presentation.map
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import campus.tech.kakao.map.R
+import campus.tech.kakao.map.databinding.BottomSheetBinding
+import campus.tech.kakao.map.domain.model.Place
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+
+class MapBottomSheet : BottomSheetDialogFragment() {
+
+ private lateinit var binding: BottomSheetBinding
+ private var place: Place? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ binding = BottomSheetBinding.inflate(inflater, container, false)
+ place = arguments?.getSerializable("place") as? Place
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.tvPlaceName.text = place?.place ?: "알림"
+ binding.tvPlaceAddress.text = place?.address ?: "원하는 장소를 검색해 주세요"
+ }
+
+}
diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/map/MapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/presentation/map/MapViewModel.kt
new file mode 100644
index 00000000..064b73e0
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/presentation/map/MapViewModel.kt
@@ -0,0 +1,34 @@
+package campus.tech.kakao.map.presentation.map
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import campus.tech.kakao.map.data.LastVisitedPlaceManager
+import campus.tech.kakao.map.domain.model.Place
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class MapViewModel
+@Inject
+constructor(private val manager: LastVisitedPlaceManager): ViewModel() {
+
+ private val _lastVisitedPlace = MutableStateFlow(null)
+ val lastVisitedPlace: StateFlow get() = _lastVisitedPlace.asStateFlow()
+ fun loadLastVisitedPlace() {
+ viewModelScope.launch(Dispatchers.IO) {
+ val place = manager.getLastVisitedPlace()
+ _lastVisitedPlace.value = place
+ }
+ }
+ fun saveLastVisitedPlace(place: Place) {
+ viewModelScope.launch(Dispatchers.IO) {
+ manager.saveLastVisitedPlace(place)
+ _lastVisitedPlace.value = place
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/campus/tech/kakao/map/presentation/search/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/presentation/search/SearchActivity.kt
new file mode 100644
index 00000000..5cdb63e0
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/presentation/search/SearchActivity.kt
@@ -0,0 +1,119 @@
+package campus.tech.kakao.map.presentation.search
+
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+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.presentation.map.MapActivity
+import campus.tech.kakao.map.util.PlaceMapper
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class SearchActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+ private lateinit var searchedPlaceAdapter: SearchedPlaceAdapter
+ private lateinit var logAdapter: LogAdapter
+ private val viewModel: SearchViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ init()
+ }
+
+ private fun init() {
+ setStatusBarTransparent()
+ initBinding()
+ setupRecyclerViews()
+ observeViewModel()
+ }
+
+ private fun setStatusBarTransparent() {
+ this.window?.apply {
+ this.statusBarColor = Color.TRANSPARENT
+ decorView.systemUiVisibility =
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ }
+ }
+
+ 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) {
+ lifecycleScope.launch {
+ val selectedPlace = viewModel.getPlaceById(place.id)
+ val intent = Intent(this@SearchActivity, MapActivity::class.java).apply {
+ putExtra("placeData", selectedPlace)
+ }
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+ }
+
+ private fun setupLogRecyclerView() {
+ val logRecyclerView = binding.recyclerLog
+ logAdapter = LogAdapter { id ->
+ lifecycleScope.launch {
+ viewModel.removeLog(id)
+ }
+ }
+
+ 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
+ }
+ }
+
+ lifecycleScope.launch {
+ viewModel.logList.collect { 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/search/SearchUiState.kt b/app/src/main/java/campus/tech/kakao/map/presentation/search/SearchUiState.kt
new file mode 100644
index 00000000..9c3db290
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/presentation/search/SearchUiState.kt
@@ -0,0 +1,10 @@
+package campus.tech.kakao.map.presentation.search
+
+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/search/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/presentation/search/SearchViewModel.kt
new file mode 100644
index 00000000..f4e314ba
--- /dev/null
+++ b/app/src/main/java/campus/tech/kakao/map/presentation/search/SearchViewModel.kt
@@ -0,0 +1,77 @@
+package campus.tech.kakao.map.presentation.search
+
+import androidx.lifecycle.*
+import campus.tech.kakao.map.domain.model.Place
+import campus.tech.kakao.map.domain.repository.PlaceRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class SearchViewModel
+@Inject
+constructor( private val repository: PlaceRepository) : ViewModel() {
+
+ val searchText = MutableLiveData()
+
+ private val _logList = MutableStateFlow>(emptyList())
+ val logList: StateFlow> get() = _logList.asStateFlow()
+
+ private val _searchedPlaces = searchText.asFlow()
+ .debounce(500L)
+ .flatMapLatest { keyword ->
+ if (keyword.isNotBlank()) {
+ flowOf(getPlaces(keyword))
+ } else {
+ flowOf(emptyList())
+ }
+ }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
+
+ val searchedPlaces: StateFlow> get() = _searchedPlaces
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ _logList.value = getLogs()
+ }
+ }
+
+ fun clearSearch() {
+ searchText.value = ""
+ }
+
+ private suspend fun getPlaces(keyword: String): List {
+ return repository.getPlaces(keyword)
+ }
+
+ suspend fun getPlaceById(id: String): Place? {
+ return repository.getPlaceById(id)
+ }
+
+ private suspend fun getLogs(): List {
+ return repository.getLogs()
+ }
+
+ fun updateLogs(place: Place) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val updatedList = _logList.value.toMutableList()
+ 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) {
+ viewModelScope.launch(Dispatchers.IO) {
+ repository.removeLog(id)
+ _logList.value = getLogs()
+ }
+ }
+}
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/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..0c7b81b5
--- /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..780edbb2
--- /dev/null
+++ b/app/src/main/res/drawable/searchview_background2.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ 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..b128ead5 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..0872aad5
--- /dev/null
+++ b/app/src/main/res/layout/activity_map.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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/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..55490cf9 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -2,6 +2,8 @@
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e708b1c0..7f93135c 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2927e499..3fa8f862 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Sat Jun 15 19:44:23 KST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 4f906e0c..1aa94a42 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,99 @@
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index ac1b06f9..6689b85b 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 48bba0fc..bb703c82 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,12 +1,6 @@
pluginManagement {
repositories {
- google {
- content {
- includeGroupByRegex("com\\.android.*")
- includeGroupByRegex("com\\.google.*")
- includeGroupByRegex("androidx.*")
- }
- }
+ google()
mavenCentral()
gradlePluginPortal()
}
@@ -17,6 +11,7 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/")
+ maven("https://devrepo.kakao.com/nexus/content/groups/public/")
}
}