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/")
}
}