diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 803085bd..58c846c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,7 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties +import java.io.FileInputStream +import java.util.Properties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -5,7 +9,7 @@ plugins { id("kotlin-parcelize") id("kotlin-kapt") id("com.google.dagger.hilt.android") - id("com.google.gms.google-services") + //id("com.google.gms.google-services") } android { @@ -20,6 +24,17 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY")) + buildConfigField("String", "KAKAO_REST_API_KEY", "\"" + getApiKey("KAKAO_REST_API_KEY") + "\"") + + ndk { + abiFilters.add("arm64-v8a") + abiFilters.add("armeabi-v7a") + abiFilters.add("x86") + abiFilters.add("x86_64") + } + } buildTypes { @@ -40,13 +55,35 @@ android { } buildFeatures { + viewBinding = true dataBinding = true buildConfig = true } } +fun getApiKey(key: String): String { + val properties = Properties() + val localPropertiesFile = rootProject.file("local.properties") + if (localPropertiesFile.exists()) { + properties.load(FileInputStream(localPropertiesFile)) + } + return properties.getProperty(key) ?: throw IllegalArgumentException("API key not found for: " + key) +} + dependencies { + 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("com.kakao.sdk:v2-all:2.20.3") + 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:1.8.0") + implementation("androidx.test:core-ktx:1.5.0") implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("com.google.android.material:material:1.12.0") @@ -74,6 +111,10 @@ dependencies { 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("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") androidTestImplementation("androidx.test.ext:junit:1.2.1") diff --git a/app/src/androidTest/java/campus/tech/kakao/map/uiTest/MainActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/uiTest/MainActivityTest.kt new file mode 100644 index 00000000..e867294c --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/uiTest/MainActivityTest.kt @@ -0,0 +1,60 @@ +package campus.tech.kakao.map.uiTest + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import campus.tech.kakao.map.R +import campus.tech.kakao.map.ui.MainActivity +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MainActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + // 검색어를 입력하면 올바른 결과가 뜨나요? + @Test + fun testEnteringKeywordCorrectResult() { + onView(withId(R.id.etKeywords)).perform(typeText("london")) + + Thread.sleep(2000) + + onView(withId(R.id.rvSearchResult)) + .check(matches(hasDescendant(withText("london1118")))) + } + + // 결과를 클릭하면 지도 화면으로 올바르게 이동하나요? + @Test + fun testClickingResultsTakeMapScreenCorrectly() { + onView(withId(R.id.etKeywords)).perform(typeText("london")) + + Thread.sleep(2000) + + onView(withText("london1118")).perform(click()) + + Thread.sleep(500) + + onView(withId(R.id.mapView)).check(matches(isDisplayed())) + } + + // 검색어 입력 후 삭제 버튼을 누르면 검색어가 잘 삭제되나요? + @Test + fun testKeywordDeletedByPressingDeleteButton() { + onView(withId(R.id.etKeywords)).perform(typeText("london")) + + onView(withId(R.id.ivClear)).perform(click()) + + onView(withId(R.id.etKeywords)).check(matches(withText(""))) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/uiTest/MapActivitiyTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/uiTest/MapActivitiyTest.kt new file mode 100644 index 00000000..aee946b7 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/uiTest/MapActivitiyTest.kt @@ -0,0 +1,36 @@ +package campus.tech.kakao.map.uiTest + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import campus.tech.kakao.map.R +import campus.tech.kakao.map.ui.MapActivity +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MapActivitiyTest { + + @get:Rule + val activityRule = ActivityScenarioRule(MapActivity::class.java) + + // 지도 화면이 잘 보이나요? + @Test + fun testMapScreenVisible() { + onView(withId(R.id.mapView)).check(matches(isDisplayed())) + } + + // 검색창을 클릭하면 검색 화면으로 잘 넘어가나요? + @Test + fun testClickingSearchBarMovesToSearchScreen() { + onView(withId(R.id.etMapSearch)).perform(click()) + + onView(withId(R.id.etKeywords)).check(matches(isDisplayed())) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/unitTest/MainActivityUnitTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/unitTest/MainActivityUnitTest.kt new file mode 100644 index 00000000..4d1b1321 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/unitTest/MainActivityUnitTest.kt @@ -0,0 +1,78 @@ +package campus.tech.kakao.map.unitTest + +import android.view.View +import android.widget.EditText +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import campus.tech.kakao.map.R +import campus.tech.kakao.map.ui.MainActivity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MainActivityUnitTest { + + private lateinit var scenario: ActivityScenario + + @Before + fun setUp() { + scenario = ActivityScenario.launch(MainActivity::class.java) + } + + @After + fun tearDown() { + scenario.close() + } + + // MainActivity가 잘 보이나요? + @Test + fun testMainActivityVisible() { + scenario.onActivity { activity -> + val mainView = activity.findViewById(R.id.main) + assertNotNull(mainView) + } + } + + // 검색 기능이 잘 동작하나요? +// @Test +// fun testVerifySearchFunction() { +// scenario.onActivity { activity -> +// val keyword = "london" +// val editText = activity.findViewById(R.id.etKeywords) +// editText.setText(keyword) +// +// activity.mapViewModel.searchPlaces(keyword) +// +// activity.mapViewModel.searchResults.observe(activity, Observer { results -> +// assertTrue(results.isNotEmpty()) +// }) +// } +// } +// +// // 검색 결과에 따라 RecyclerView가 잘 바뀌나요? +// @Test +// fun testRecyclerViewVisibility() { +// scenario.onActivity { activity -> +// val recyclerView = activity.findViewById(R.id.rvSearchResult) +// val noResultTextView = activity.findViewById(R.id.tvNoResults) +// +// activity.mapViewModel.searchResults.observe(activity, Observer { results -> +// if (results.isEmpty()) { +// assertEquals(View.VISIBLE, noResultTextView.visibility) +// assertEquals(View.GONE, recyclerView.visibility) +// } else { +// assertEquals(View.GONE, noResultTextView.visibility) +// assertEquals(View.VISIBLE, recyclerView.visibility) +// } +// }) +// } +// } +} \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/unitTest/MapActivityUnitTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/unitTest/MapActivityUnitTest.kt new file mode 100644 index 00000000..a53ae658 --- /dev/null +++ b/app/src/androidTest/java/campus/tech/kakao/map/unitTest/MapActivityUnitTest.kt @@ -0,0 +1,80 @@ +package campus.tech.kakao.map.unitTest + +import android.content.Context +import android.view.View +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import campus.tech.kakao.map.R +import campus.tech.kakao.map.ui.MapActivity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MapActivityUnitTest { + + private lateinit var scenario: ActivityScenario + + @Before + fun setup() { + scenario = ActivityScenario.launch(MapActivity::class.java) + } + + @After + fun tearDown() { + scenario.close() + } + + // MapActivity가 잘 보이나요? + @Test + fun testMapActivityVisible() { + scenario.onActivity { activity -> + val mainView = activity.findViewById(R.id.main) + assertNotNull(mainView) + } + } + + // 저장된 위치를 잘 로드하나요? + @Test + fun testSavedLocationLoad() { + scenario.onActivity { activity -> + val preferences = activity.getSharedPreferences("location_prefs", Context.MODE_PRIVATE) + val editor = preferences.edit() + val savedLatitude = 37.3957122 + val savedLongitude = 127.1105181 + editor.putString("latitude", savedLatitude.toString()) + editor.putString("longitude", savedLongitude.toString()) + editor.apply() + + activity.loadSavedLocation() + + assertEquals(savedLatitude, activity.savedLatitude) + assertEquals(savedLongitude, activity.savedLongitude) + } + } + + // 현재 위치를 잘 저장하나요? + @Test + fun testSaveCurrentLocation() { + scenario.onActivity { activity -> + val latitude = 37.0 + val longitude = 127.0 + + activity.savedLatitude = latitude + activity.savedLongitude = longitude + + activity.saveCurrentLocation() + + val preferences = activity.getSharedPreferences("location_prefs", Context.MODE_PRIVATE) + val savedLatitude = preferences.getString("latitude", null)?.toDouble() + val savedLongitude = preferences.getString("longitude", null)?.toDouble() + + assertEquals(latitude, savedLatitude) + assertEquals(longitude, savedLongitude) + } + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f2e7b45a..1001628e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,16 +15,23 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Map" - tools:targetApi="31"> + tools:targetApi="31" + android:name=".application.AppClass" + android:networkSecurityConfig="@xml/network_security_config"> + + android:name=".ui.MapActivity" + android:exported="true" > - - - + + + + + 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/application/AppClass.kt b/app/src/main/java/campus/tech/kakao/map/application/AppClass.kt new file mode 100644 index 00000000..2ae7972e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/application/AppClass.kt @@ -0,0 +1,21 @@ +package campus.tech.kakao.map.application + +import android.app.Application +import campus.tech.kakao.map.R +import campus.tech.kakao.map.database.AppDatabase +import com.kakao.vectormap.KakaoMapSdk +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject + +@HiltAndroidApp +class AppClass : Application() { + + @Inject + lateinit var database: AppDatabase + + override fun onCreate() { + super.onCreate() + KakaoMapSdk.init(this, getString(R.string.kakao_api_key)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/database/AppDatabase.kt b/app/src/main/java/campus/tech/kakao/map/database/AppDatabase.kt new file mode 100644 index 00000000..4f5a8ea3 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/database/AppDatabase.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import campus.tech.kakao.map.model.MapItemEntity + +@Database(entities = [MapItemEntity::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + + abstract fun mapItemDao(): MapItemDao + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/database/DbHelper.kt b/app/src/main/java/campus/tech/kakao/map/database/DbHelper.kt new file mode 100644 index 00000000..1a116a2b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/database/DbHelper.kt @@ -0,0 +1,119 @@ +package campus.tech.kakao.map.database +/* +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class DbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + + companion object { + private const val DATABASE_NAME = "location.db" + private const val DATABASE_VERSION = 1 + } + + override fun onCreate(db: SQLiteDatabase) { + + val createCafeTable = """ + CREATE TABLE ${MapContract.TABLE_CAFE} ( + ${MapContract.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${MapContract.COLUMN_NAME} TEXT, + ${MapContract.COLUMN_ADDRESS} TEXT, + ${MapContract.COLUMN_CATEGORY} TEXT + ) + """.trimIndent() + + val createPharmacyTable = """ + CREATE TABLE ${MapContract.TABLE_PHARMACY} ( + ${MapContract.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${MapContract.COLUMN_NAME} TEXT, + ${MapContract.COLUMN_ADDRESS} TEXT, + ${MapContract.COLUMN_CATEGORY} TEXT + ) + """.trimIndent() + + db.execSQL(createCafeTable) + db.execSQL(createPharmacyTable) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("DROP TABLE IF EXISTS ${MapContract.TABLE_CAFE}") + db.execSQL("DROP TABLE IF EXISTS ${MapContract.TABLE_PHARMACY}") + onCreate(db) + } + + // step1 피드백 수정 부분 + fun insertData() { + val db = writableDatabase + + for (i in 1..9) { + val name = "카페$i" + // 데이터가 있는지 체크 후 데이터 삽입 처리 + if (!dataExists(MapContract.TABLE_CAFE, name)) { + val insertCafe = """ + INSERT INTO ${MapContract.TABLE_CAFE} (${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY}) + VALUES ('$name', '서울 성동구 성수동 $i', '카페') + """.trimIndent() + db.execSQL(insertCafe) + } + } + + for (i in 1..9) { + val name = "약국$i" + if (!dataExists(MapContract.TABLE_PHARMACY, name)) { + val insertPharmacy = """ + INSERT INTO ${MapContract.TABLE_PHARMACY} (${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY}) + VALUES ('$name', '서울 강남구 대치동 $i', '약국') + """.trimIndent() + db.execSQL(insertPharmacy) + } + } + + db.close() + } + + // 데이터 존재 여부 체킹 메소드 + private fun dataExists(tableName: String, name: String): Boolean { + val db = readableDatabase + val query = "SELECT 1 FROM $tableName WHERE ${MapContract.COLUMN_NAME} = ?" + val cursor = db.rawQuery(query, arrayOf(name)) + val exists = cursor.moveToFirst() + cursor.close() + return exists + } + + fun searchPlaces(keyword: String): List { + val db = readableDatabase + val query = """ + SELECT ${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY} + FROM ${MapContract.TABLE_CAFE} + WHERE ${MapContract.COLUMN_NAME} LIKE ? + UNION + SELECT ${MapContract.COLUMN_NAME}, ${MapContract.COLUMN_ADDRESS}, ${MapContract.COLUMN_CATEGORY} + FROM ${MapContract.TABLE_PHARMACY} + WHERE ${MapContract.COLUMN_NAME} LIKE ? + """.trimIndent() + val cursor = db.rawQuery(query, arrayOf("%$keyword%", "%$keyword%")) + val results = mutableListOf() + + cursor.use { + val nameIndex = cursor.getColumnIndex(MapContract.COLUMN_NAME) + val addressIndex = cursor.getColumnIndex(MapContract.COLUMN_ADDRESS) + val categoryIndex = cursor.getColumnIndex(MapContract.COLUMN_CATEGORY) + + if (nameIndex != -1 && addressIndex != -1 && categoryIndex != -1) { + if (cursor.moveToFirst()) { + do { + val name = cursor.getString(nameIndex) + val address = cursor.getString(addressIndex) + val category = cursor.getString(categoryIndex) + results.add(MapItem(name, address, category)) + } while (cursor.moveToNext()) + } + } + } + + return results + } +} +*/ diff --git a/app/src/main/java/campus/tech/kakao/map/database/MapContract.kt b/app/src/main/java/campus/tech/kakao/map/database/MapContract.kt new file mode 100644 index 00000000..3a99a571 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/database/MapContract.kt @@ -0,0 +1,12 @@ +package campus.tech.kakao.map.database + +object MapContract +{ + const val TABLE_CAFE = "Cafe" + const val TABLE_PHARMACY = "Pharmacy" + + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_ADDRESS = "address" + const val COLUMN_CATEGORY = "category" +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/database/MapItemDao.kt b/app/src/main/java/campus/tech/kakao/map/database/MapItemDao.kt new file mode 100644 index 00000000..730cb5d7 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/database/MapItemDao.kt @@ -0,0 +1,24 @@ +package campus.tech.kakao.map.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import campus.tech.kakao.map.model.MapItemEntity + +@Dao +interface MapItemDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(mapItem: MapItemEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(mapItems: List) + + @Query("SELECT * FROM mapItems") + suspend fun getAllMapItems(): List + + @Query("DELETE FROM mapItems") + suspend fun deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/di/AppModule.kt b/app/src/main/java/campus/tech/kakao/map/di/AppModule.kt new file mode 100644 index 00000000..d124d375 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/di/AppModule.kt @@ -0,0 +1,42 @@ +package campus.tech.kakao.map.di + +import android.content.Context +import androidx.room.Room +import campus.tech.kakao.map.database.AppDatabase +import campus.tech.kakao.map.database.MapItemDao +import campus.tech.kakao.map.repository.MapItemRepository +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 AppModule { + + @Provides + @Singleton + fun provideDatabse(@ApplicationContext appContext: Context): AppDatabase { + return Room.databaseBuilder( + appContext, + AppDatabase::class.java, + "mapItemDatabase" + ).build() + } + + @Provides + fun provideMapItemDao(db: AppDatabase): MapItemDao { + return db.mapItemDao() + } + + @Provides + @Singleton + fun provideMapItemRepository( + mapItemDao: MapItemDao, + @ApplicationContext context: Context + ): MapItemRepository { + return MapItemRepository(mapItemDao, context) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/KakaoMapProductResponse.kt b/app/src/main/java/campus/tech/kakao/map/model/KakaoMapProductResponse.kt new file mode 100644 index 00000000..f2ba6944 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/KakaoMapProductResponse.kt @@ -0,0 +1,13 @@ +package campus.tech.kakao.map.model + +data class KakaoMapProductResponse( + val documents: List +) + +data class Document( + val place_name: String, + val address_name: String, + val category_group_name: String, + val x: String, + val y: String +) diff --git a/app/src/main/java/campus/tech/kakao/map/model/MapItemEntity.kt b/app/src/main/java/campus/tech/kakao/map/model/MapItemEntity.kt new file mode 100644 index 00000000..3e22e0a4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/model/MapItemEntity.kt @@ -0,0 +1,32 @@ +package campus.tech.kakao.map.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "mapItems") +data class MapItemEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val name: String, + val address: String, + val category: String, + val longitude: Double, + val latitude: Double +) + +data class MapItem( + val name: String, + val address: String, + val category: String, + val longitude: Double, + val latitude: Double +) + +fun MapItem.toEntity(): MapItemEntity { + return MapItemEntity( + name = this.name, + address = this.address, + category = this.category, + longitude = this.longitude, + latitude = this.latitude + ) +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/network/KakaoMapRetrofitService.kt b/app/src/main/java/campus/tech/kakao/map/network/KakaoMapRetrofitService.kt new file mode 100644 index 00000000..6c4458d7 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/network/KakaoMapRetrofitService.kt @@ -0,0 +1,15 @@ +package campus.tech.kakao.map.network + +import campus.tech.kakao.map.model.KakaoMapProductResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface KakaoMapRetrofitService { + @GET("v2/local/search/keyword.json") + suspend fun searchPlaces( + @Header("Authorization") apiKey: String, + @Query("query") query: String + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/network/RetrofitClient.kt b/app/src/main/java/campus/tech/kakao/map/network/RetrofitClient.kt new file mode 100644 index 00000000..5ea387a8 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/network/RetrofitClient.kt @@ -0,0 +1,23 @@ +package campus.tech.kakao.map.network + +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitClient { + private const val BASE_URL = "https://dapi.kakao.com/" + + private val client = OkHttpClient.Builder().build() + + private val instance: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + val apiService: KakaoMapRetrofitService by lazy { + instance.create(KakaoMapRetrofitService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/MapItemRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/MapItemRepository.kt new file mode 100644 index 00000000..a1afa56e --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/repository/MapItemRepository.kt @@ -0,0 +1,47 @@ +package campus.tech.kakao.map.repository + +import android.content.Context +import campus.tech.kakao.map.database.MapItemDao +import campus.tech.kakao.map.model.MapItemEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MapItemRepository @Inject constructor( + private val mapItemDao: MapItemDao, + private val context: Context) { + + suspend fun insertAll(mapItems: List) { + mapItemDao.insertAll(mapItems) + } + + suspend fun getAllMapItems(): List { + return mapItemDao.getAllMapItems() + } + + suspend fun deleteAll() { + withContext(Dispatchers.IO) { + mapItemDao.deleteAll() + } + } + + suspend fun loadKeywords(): List { + return withContext(Dispatchers.IO) { + val sharedPreferences = context.getSharedPreferences("keywords", Context.MODE_PRIVATE) + val keywordsSet = sharedPreferences.getStringSet("keywords", setOf()) ?: setOf() + keywordsSet.toList() + } + } + + suspend fun saveKeywords(keywords: List) { + withContext(Dispatchers.IO) { + val sharedPreferences = context.getSharedPreferences("keywords", Context.MODE_PRIVATE) + sharedPreferences.edit().apply { + putStringSet("keywords", keywords.toSet()) + apply() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/BindingAdapters.kt b/app/src/main/java/campus/tech/kakao/map/ui/BindingAdapters.kt new file mode 100644 index 00000000..06cb41d0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/BindingAdapters.kt @@ -0,0 +1,33 @@ +package campus.tech.kakao.map.ui +// +//import android.widget.EditText +//import androidx.databinding.BindingAdapter +//import androidx.databinding.InverseBindingAdapter +//import androidx.databinding.InverseBindingListener +//import androidx.lifecycle.MutableLiveData +// +//@BindingAdapter("app:keyword") +//fun setKeyword(editText: EditText, keyword: MutableLiveData?) { +// if (keyword != null && editText.text.toString() != keyword.value) { +// editText.setText(keyword.value) +// } +//} +// +//@InverseBindingAdapter(attribute = "app:keyword") +//fun getKeyword(editText: EditText): String { +// return editText.text.toString() +//} +// +//@BindingAdapter("app:keywordAttrChanged") +//fun setKeywordListener(editText: EditText, listener: InverseBindingListener?) { +// if (listener != null) { +// editText.addTextChangedListener(object : android.text.TextWatcher { +// override fun afterTextChanged(s: android.text.Editable?) { +// listener.onChange() +// } +// +// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} +// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} +// }) +// } +//} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/KeywordAdapter.kt b/app/src/main/java/campus/tech/kakao/map/ui/KeywordAdapter.kt new file mode 100644 index 00000000..d172de61 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/KeywordAdapter.kt @@ -0,0 +1,64 @@ +package campus.tech.kakao.map.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R + +class KeywordAdapter(private val listener: OnKeywordRemoveListener) : RecyclerView.Adapter() { + + interface OnKeywordRemoveListener { + fun onKeywordRemove(keyword: String) + fun onKeywordClick(keyword: String) + } + + private val keywords = mutableListOf() + + val currentKeywords: List + get() = keywords + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_keyword, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val keyword = keywords[position] + holder.bind(keyword) + } + + override fun getItemCount(): Int { + return keywords.size + } + + fun submitList(newKeywords: List) { + val oldSize = keywords.size + keywords.clear() + notifyItemRangeRemoved(0, oldSize) + keywords.addAll(newKeywords) + notifyItemRangeInserted(0, newKeywords.size) + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val tvKeyword: TextView = itemView.findViewById(R.id.tvKeyword) + private val ivRemove: ImageView = itemView.findViewById(R.id.imageView) + + fun bind(keyword: String) { + tvKeyword.text = keyword + ivRemove.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + keywords.removeAt(position) + notifyItemRemoved(position) + listener.onKeywordRemove(keyword) + } + } + itemView.setOnClickListener { + listener.onKeywordClick(keyword) + } + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt new file mode 100644 index 00000000..38e25ed0 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt @@ -0,0 +1,104 @@ +package campus.tech.kakao.map.ui + +import android.content.Intent +import android.os.Bundle +import android.widget.TextView +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.viewmodel.MapViewModel +import campus.tech.kakao.map.databinding.ActivityMainBinding +import campus.tech.kakao.map.model.MapItem +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : AppCompatActivity(), SearchResultAdapter.OnItemClickListener, + KeywordAdapter.OnKeywordRemoveListener { + + private val mapViewModel: MapViewModel by viewModels() + private lateinit var binding: ActivityMainBinding + + private val searchResultAdapter = SearchResultAdapter(this) + private val keywordAdapter = KeywordAdapter(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + binding.lifecycleOwner = this + binding.viewModel = mapViewModel + + binding.rvSearchResult.adapter = searchResultAdapter + binding.rvKeywords.adapter = keywordAdapter + + mapViewModel.searchResults.observe(this, Observer { results -> + if (results.isEmpty()) { + binding.tvNoResults.visibility = TextView.VISIBLE + binding.rvSearchResult.visibility = RecyclerView.GONE + } else { + binding.tvNoResults.visibility = TextView.GONE + binding.rvSearchResult.visibility = RecyclerView.VISIBLE + searchResultAdapter.submitList(results) + } + }) + + mapViewModel.errorMessage.observe(this, Observer { message -> + message?.let { + Toast.makeText(this, it, Toast.LENGTH_LONG).show() + } + }) + + mapViewModel.keywords.observe(this, Observer { keywords -> + keywordAdapter.submitList(keywords) + }) + + binding.ivClear.setOnClickListener { + mapViewModel.clearKeyword() + } + + mapViewModel.loadKeywords() + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val intent = Intent(this@MainActivity, MapActivity::class.java) + startActivity(intent) + finish() + } + }) + + mapViewModel.getSavedMapItems() + + } + + override fun onItemClick(item: MapItem) { + val newKeywords = keywordAdapter.currentKeywords.toMutableList() + + if (!newKeywords.contains(item.name)) { + newKeywords.add(item.name) + mapViewModel.saveKeywords(newKeywords) + } + + val intent = Intent(this, MapActivity::class.java).apply { + putExtra("name", item.name) + putExtra("address", item.address) + putExtra("longitude", item.longitude) + putExtra("latitude", item.latitude) + } + startActivity(intent) + } + + override fun onKeywordRemove(keyword: String) { + val newKeywords = keywordAdapter.currentKeywords.toMutableList().apply { remove(keyword) } + mapViewModel.saveKeywords(newKeywords) + } + + override fun onKeywordClick(keyword: String) { + binding.etKeywords.setText(keyword) + mapViewModel.setKeyword(keyword) + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/MapActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/MapActivity.kt new file mode 100644 index 00000000..77341599 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/MapActivity.kt @@ -0,0 +1,171 @@ +package campus.tech.kakao.map.ui + +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.EditText +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import campus.tech.kakao.map.R +import campus.tech.kakao.map.databinding.ActivityMapBinding +import campus.tech.kakao.map.databinding.BottomSheetBinding +import campus.tech.kakao.map.databinding.ErrorLayoutBinding +import campus.tech.kakao.map.viewmodel.MapViewModel +import com.google.android.material.bottomsheet.BottomSheetDialog +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 + +@AndroidEntryPoint +class MapActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMapBinding + private lateinit var mapView: MapView + private var kakaoMap: KakaoMap? = null + var savedLatitude: Double = 37.3957122 + var savedLongitude: Double = 127.1105181 + private lateinit var errorBinding: ErrorLayoutBinding + + private val mapViewModel: MapViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_map) + binding.lifecycleOwner = this + binding.viewModel = mapViewModel + + mapView = findViewById(R.id.mapView) + loadSavedLocation() + + mapView.start(mapLifeCycleCallback, kakaoMapReadyCallback) + + val etMapSearch = findViewById(R.id.etMapSearch) + etMapSearch.setOnClickListener { + mapViewModel.clearMapItems() + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + } + } + + override fun onResume() { + super.onResume() + mapView.resume() + } + + override fun onPause() { + super.onPause() + mapView.pause() + saveCurrentLocation() + } + + private val mapLifeCycleCallback = object : MapLifeCycleCallback() { + override fun onMapDestroy() { + Log.d("MapActivity", getString(R.string.onMapDestroyLog)) + } + + override fun onMapError(error: Exception) { + Log.e("MapActivity", getString(R.string.onMapErrorLog)) + showErrorLayout(error.message) + } + } + + private val kakaoMapReadyCallback = object : KakaoMapReadyCallback() { + override fun onMapReady(map: KakaoMap) { + Log.d("MapActivity", getString(R.string.onMapReadyLog)) + kakaoMap = map + updateCameraPosition(savedLatitude, savedLongitude) + + val name = intent.getStringExtra("name") + val address = intent.getStringExtra("address") + val longitude = intent.getDoubleExtra("longitude", 0.0) + val latitude = intent.getDoubleExtra("latitude", 0.0) + + if (latitude != 0.0 && longitude != 0.0) { + addMarker(latitude, longitude, name ?: "") + saveLocation(latitude, longitude) + showBottomSheet(name ?: "", address ?: "") + } + } + } + + private fun saveLocation(latitude: Double, longitude: Double) { + savedLatitude = latitude + savedLongitude = longitude + saveDataToPreferences(latitude.toString(), longitude.toString()) + } + + fun saveCurrentLocation() { + saveDataToPreferences(savedLatitude.toString(), savedLongitude.toString()) + } + + private fun saveDataToPreferences(latitude: String, longitude: String) { + val preferences = getSharedPreferences("location_prefs", MODE_PRIVATE) + preferences.edit().apply { + putString("latitude", latitude) + putString("longitude", longitude) + apply() + } + } + + fun loadSavedLocation() { + val preferences = getSharedPreferences("location_prefs", MODE_PRIVATE) + savedLatitude = preferences.getString("latitude", "37.3957122")?.toDouble() ?: 37.3957122 + savedLongitude = preferences.getString("longitude", "127.1105181")?.toDouble() ?: 127.1105181 + } + + private fun updateCameraPosition(latitude: Double, longitude: Double) { + val cameraUpdate = CameraUpdateFactory.newCenterPosition(LatLng.from(latitude, longitude)) + kakaoMap?.moveCamera(cameraUpdate) + } + + private fun addMarker(latitude: Double, longitude: Double, name: String) { + val labelManager = kakaoMap?.labelManager + val iconAndTextStyle = LabelStyles.from( + LabelStyle.from(R.drawable.gps).setTextStyles(30, Color.BLACK) + ) + val options = LabelOptions.from(LatLng.from(latitude, longitude)) + .setStyles(iconAndTextStyle) + val layer = labelManager?.layer + val label = layer?.addLabel(options) + label?.changeText(name) + + updateCameraPosition(latitude, longitude) + } + + private fun showBottomSheet(name: String, address: String) { + mapViewModel.setSelectedPlace(name, address) + val bottomSheetDialog = BottomSheetDialog(this) + val bottomSheetBinding: BottomSheetBinding = DataBindingUtil.inflate( + LayoutInflater.from(this), + R.layout.bottom_sheet, + null, + false + ) + bottomSheetBinding.lifecycleOwner = bottomSheetDialog + bottomSheetBinding.viewModel = mapViewModel + bottomSheetDialog.setContentView(bottomSheetBinding.root) + bottomSheetDialog.show() + } + + private fun showErrorLayout(message: String?) { + if (!::errorBinding.isInitialized) { + errorBinding = DataBindingUtil.setContentView(this, R.layout.error_layout) + errorBinding.lifecycleOwner = this + errorBinding.viewModel = mapViewModel + } + errorBinding.message = message + binding.searchLayout.visibility = if (message != null) View.GONE else View.VISIBLE + binding.mapView.visibility = if (message != null) View.GONE else View.VISIBLE + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/ui/SearchResultAdapter.kt b/app/src/main/java/campus/tech/kakao/map/ui/SearchResultAdapter.kt new file mode 100644 index 00000000..19fbb009 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/ui/SearchResultAdapter.kt @@ -0,0 +1,61 @@ +package campus.tech.kakao.map.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import campus.tech.kakao.map.R +import campus.tech.kakao.map.model.MapItem + +class SearchResultAdapter(private val listener: OnItemClickListener) : ListAdapter( + DiffCallback() +) { + + interface OnItemClickListener { + fun onItemClick(item: MapItem) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_search_result, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item) + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val tvPlaceName: TextView = itemView.findViewById(R.id.tvPlaceName) + private val tvPlaceAddress: TextView = itemView.findViewById(R.id.tvPlaceAddress) + private val tvCategory: TextView = itemView.findViewById(R.id.tvCategory) + + init { + itemView.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onItemClick(getItem(position)) + } + } + } + + fun bind(item: MapItem) { + tvPlaceName.text = item.name + tvPlaceAddress.text = item.address + tvCategory.text = item.category + } + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MapItem, newItem: MapItem): Boolean { + return oldItem.name == newItem.name && oldItem.address == newItem.address + } + + override fun areContentsTheSame(oldItem: MapItem, newItem: MapItem): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt new file mode 100644 index 00000000..ea4b619b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt @@ -0,0 +1,137 @@ +package campus.tech.kakao.map.viewmodel + +import android.app.Application +import android.content.Context +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import campus.tech.kakao.map.BuildConfig +import campus.tech.kakao.map.model.MapItem +import campus.tech.kakao.map.model.toEntity +import campus.tech.kakao.map.network.RetrofitClient +import campus.tech.kakao.map.repository.MapItemRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class MapViewModel @Inject constructor( + application: Application, + private val repository: MapItemRepository +) : AndroidViewModel(application) { + + private val _keywords = MutableLiveData>() + val keywords: LiveData> get() = _keywords + + val keyword = MutableLiveData() + + private val _searchResults = MutableLiveData>() + val searchResults: LiveData> get() = _searchResults + + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData get() = _errorMessage + + private val _selectedPlaceName = MutableLiveData() + val selectedPlaceName: LiveData get() = _selectedPlaceName + + private val _selectedPlaceAddress = MutableLiveData() + val selectedPlaceAddress: LiveData get() = _selectedPlaceAddress + + init { + keyword.observeForever { + if (!it.isNullOrEmpty()) { + searchPlaces(it) + } else { + _searchResults.postValue(emptyList()) + } + } + } + + fun setKeyword(keyword: String) { + this.keyword.value = keyword + } + + fun clearKeyword() { + keyword.value = "" + } + + private fun searchPlaces(keyword: String) { + viewModelScope.launch { + val apiKey = "KakaoAK ${BuildConfig.KAKAO_REST_API_KEY}" + try { + val response = withContext(Dispatchers.IO) { + RetrofitClient.apiService.searchPlaces(apiKey, keyword) + } + if (response.isSuccessful) { + val documents = response.body()?.documents ?: emptyList() + val results = documents.map { + MapItem(it.place_name, it.address_name, it.category_group_name, it.x.toDouble(), it.y.toDouble()) + } + _searchResults.postValue(results) + saveResultsToDatabase(results) + } else { + _searchResults.postValue(emptyList()) + _errorMessage.postValue("Error: ${response.message()}") + } + } catch (e: Exception) { + _searchResults.postValue(emptyList()) + _errorMessage.postValue("네트워크 요청 실패: ${e.message}") + } + } + } + + private fun saveResultsToDatabase(results: List) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repository.deleteAll() + val entities = results.map { it.toEntity() } + repository.insertAll(entities) + } + } + } + + fun getSavedMapItems() { + viewModelScope.launch { + val savedItems = withContext(Dispatchers.IO) { + repository.getAllMapItems() + } + _searchResults.postValue(savedItems.map { + MapItem(it.name, it.address, it.category, it.longitude, it.latitude) + }) + } + } + + fun clearMapItems() { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repository.deleteAll() + } + } + } + + fun loadKeywords() { + viewModelScope.launch { + val keywords = withContext(Dispatchers.IO) { + repository.loadKeywords() + } + _keywords.postValue(keywords) + } + } + + fun saveKeywords(keywords: List) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repository.saveKeywords(keywords) + } + _keywords.postValue(keywords) + } + } + + fun setSelectedPlace(name: String, address: String) { + _selectedPlaceName.value = name + _selectedPlaceAddress.value = address + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/close_24px.xml b/app/src/main/res/drawable/close_24px.xml new file mode 100644 index 00000000..7a0ff35d --- /dev/null +++ b/app/src/main/res/drawable/close_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/gps.png b/app/src/main/res/drawable/gps.png new file mode 100644 index 00000000..de3a6a20 Binary files /dev/null and b/app/src/main/res/drawable/gps.png differ diff --git a/app/src/main/res/drawable/location_on_24px.xml b/app/src/main/res/drawable/location_on_24px.xml new file mode 100644 index 00000000..871f7cee --- /dev/null +++ b/app/src/main/res/drawable/location_on_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/refresh_24px.xml b/app/src/main/res/drawable/refresh_24px.xml new file mode 100644 index 00000000..f4302a5c --- /dev/null +++ b/app/src/main/res/drawable/refresh_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/search_24px.xml b/app/src/main/res/drawable/search_24px.xml new file mode 100644 index 00000000..390774bb --- /dev/null +++ b/app/src/main/res/drawable/search_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..99832bfd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,85 @@ - - - - - - + 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..3e0fe342 --- /dev/null +++ b/app/src/main/res/layout/activity_map.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + 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..e88c5d73 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/error_layout.xml b/app/src/main/res/layout/error_layout.xml new file mode 100644 index 00000000..02ca6b2e --- /dev/null +++ b/app/src/main/res/layout/error_layout.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_keyword.xml b/app/src/main/res/layout/item_keyword.xml new file mode 100644 index 00000000..a8c3bac3 --- /dev/null +++ b/app/src/main/res/layout/item_keyword.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml new file mode 100644 index 00000000..4752be00 --- /dev/null +++ b/app/src/main/res/layout/item_search_result.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5ba5b9c..4bc21f24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,19 @@ Map + 검색어를 입력해 주세요. + 검색 결과가 없습니다. + 검색어 초기화 버튼 + keywordCancle + keyword + locationMarker + place name + place address + category + 검색어 돋보기 이미지 + 지도 API 정상 종료 + 지도 API 인증 실패 + 지도 API 정상 실행 + 지도 인증을 실패했습니다.\n다시 시도해주세요. + 에러 메세지 + 로딩 이미지 \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..96ed3bbe --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 54.180.95.212 + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2927e499..d676f0c7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,5 +2,7 @@ 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/settings.gradle.kts b/settings.gradle.kts index e3b43d09..6b0478fd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ dependencyResolutionManagement { google() mavenCentral() maven("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") + maven("https://devrepo.kakao.com/nexus/content/groups/public/") } }