diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..c5e1da4
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 7b3006b..bafed18 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -11,6 +11,7 @@
+
diff --git a/build.gradle.kts b/build.gradle.kts
index 23f3a06..bbe1666 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,6 +2,7 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.android.library) apply false
// alias(libs.plugins.secrets.gradle.plugin) apply false
}
diff --git a/data/.gitignore b/data/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/data/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
new file mode 100644
index 0000000..fcd20b9
--- /dev/null
+++ b/data/build.gradle.kts
@@ -0,0 +1,51 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "ny.photomap.data"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 29
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.exifinterface)
+ testImplementation(libs.junit)
+ testImplementation(libs.mockito.kotlin)
+ testImplementation(libs.robolectric)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/data/consumer-rules.pro b/data/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/data/proguard-rules.pro b/data/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/data/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/data/src/androidTest/java/ny/photomap/data/ExampleInstrumentedTest.kt b/data/src/androidTest/java/ny/photomap/data/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..867ea2e
--- /dev/null
+++ b/data/src/androidTest/java/ny/photomap/data/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package ny.photomap.data
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("ny.photomap.data.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/data/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/data/src/main/java/ny/photomap/data/PhotoDataSource.kt b/data/src/main/java/ny/photomap/data/PhotoDataSource.kt
new file mode 100644
index 0000000..c799cc6
--- /dev/null
+++ b/data/src/main/java/ny/photomap/data/PhotoDataSource.kt
@@ -0,0 +1,54 @@
+package ny.photomap.data
+
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.database.Cursor
+import android.net.Uri
+import android.provider.MediaStore
+
+class PhotoDataSource(private val contentResolver: ContentResolver) {
+
+ private val projection = arrayOf(
+ MediaStore.Images.Media._ID,
+ MediaStore.Images.Media.DATE_TAKEN
+ )
+
+ private val order =
+ "${MediaStore.Images.Media.DATE_TAKEN}, ${MediaStore.Images.Media.DATE_ADDED}, ${MediaStore.Images.Media.DATE_MODIFIED} DESC"
+
+ fun getAllPhotoUriList(): List = getPhotoUriList(null, null)
+ fun getDateRangePhotoUriList(startMilliSecond: Long, endMilliSecond: Long): List =
+ getPhotoUriList(
+ "${MediaStore.Images.Media.DATE_TAKEN} BETWEEN ? AND ?",
+ arrayOf(startMilliSecond.toString(), endMilliSecond.toString())
+ )
+
+ private fun getPhotoUriList(selection: String?, selectionArgs: Array?): List {
+ val photoUriList = mutableListOf()
+ try {
+ val query: Cursor? = contentResolver.query(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ selection,
+ selectionArgs,
+ order,
+ null
+ )
+
+ query?.use { cursor ->
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
+
+ while (cursor.moveToNext()) {
+ val id = cursor.getLong(idColumn)
+ val uri =
+ ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
+ photoUriList.add(uri)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return photoUriList
+ }
+
+}
\ No newline at end of file
diff --git a/data/src/main/java/ny/photomap/data/PhotoRepository.kt b/data/src/main/java/ny/photomap/data/PhotoRepository.kt
new file mode 100644
index 0000000..35c92e5
--- /dev/null
+++ b/data/src/main/java/ny/photomap/data/PhotoRepository.kt
@@ -0,0 +1,83 @@
+package ny.photomap.data
+
+import android.content.ContentResolver
+import android.net.Uri
+import androidx.exifinterface.media.ExifInterface
+import ny.photomap.model.PhotoInfo
+
+class PhotoRepository(val contentResolver: ContentResolver, val dataSource: PhotoDataSource) {
+
+ fun getAllPhotoInfoList(): List {
+ return dataSource.getAllPhotoUriList().mapNotNull {
+ convertPhotoInfo(it)
+ }
+ }
+
+ fun getDateRangePhotoInfoList(startMilliSecond: Long, endMilliSecond: Long): List {
+ return dataSource.getDateRangePhotoUriList(
+ startMilliSecond = startMilliSecond,
+ endMilliSecond = endMilliSecond
+ ).mapNotNull {
+ convertPhotoInfo(it)
+ }
+ }
+
+ fun getLocationPhotoInfoList(
+ targetLatitude: Double,
+ targetLongitude: Double,
+ surroundRange: Double,
+ ): List {
+ return dataSource.getAllPhotoUriList().mapNotNull {
+ convertPhotoInfo(
+ uri = it,
+ targetLatitude = targetLatitude,
+ targetLongitude = targetLongitude,
+ surroundRange = surroundRange
+ )
+ }
+ }
+
+ fun convertPhotoInfo(uri: Uri): PhotoInfo? = getExifInterface(uri)?.let {
+ it.latLong?.let { (latitude, longitude) ->
+ PhotoInfo(
+ uri = uri,
+ latitude = latitude,
+ longitude = longitude,
+ generationTime = it.generationTime
+ )
+ }
+ }
+
+ fun convertPhotoInfo(
+ uri : Uri,
+ targetLatitude: Double,
+ targetLongitude: Double,
+ surroundRange: Double,
+ ): PhotoInfo? =
+ getExifInterface(uri)?.let {
+ it.latLong?.let { (latitude, longitude) ->
+ // todo 근처 위치 판단 로직
+ if (targetLatitude in latitude - surroundRange..latitude + surroundRange
+ && targetLongitude in longitude - surroundRange..longitude + surroundRange
+ ) {
+ PhotoInfo(
+ uri = uri,
+ latitude = latitude,
+ longitude = longitude,
+ generationTime = it.generationTime
+ )
+ } else null
+ }
+ }
+
+
+ fun getExifInterface(uri: Uri): ExifInterface? =
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ ExifInterface(inputStream)
+ }
+
+ val ExifInterface.generationTime: String?
+ get() = getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)
+ ?: getAttribute(ExifInterface.TAG_DATETIME)
+ ?: getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED)
+}
\ No newline at end of file
diff --git a/data/src/main/java/ny/photomap/model/PhotoInfo.kt b/data/src/main/java/ny/photomap/model/PhotoInfo.kt
new file mode 100644
index 0000000..d286aeb
--- /dev/null
+++ b/data/src/main/java/ny/photomap/model/PhotoInfo.kt
@@ -0,0 +1,10 @@
+package ny.photomap.model
+
+import android.net.Uri
+
+data class PhotoInfo(
+ val uri: Uri,
+ val latitude: Double,
+ val longitude: Double,
+ val generationTime: String?,
+)
diff --git a/data/src/test/java/ny/photomap/data/ExampleUnitTest.kt b/data/src/test/java/ny/photomap/data/ExampleUnitTest.kt
new file mode 100644
index 0000000..e880bdd
--- /dev/null
+++ b/data/src/test/java/ny/photomap/data/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package ny.photomap.data
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/ny/photomap/data/MockDataSourceData.kt b/data/src/test/java/ny/photomap/data/MockDataSourceData.kt
new file mode 100644
index 0000000..760be13
--- /dev/null
+++ b/data/src/test/java/ny/photomap/data/MockDataSourceData.kt
@@ -0,0 +1,94 @@
+package ny.photomap.data
+
+import android.content.ContentUris
+import android.net.Uri
+import android.provider.MediaStore
+import ny.photomap.model.PhotoInfo
+
+val IMAGE_COLUMNS = arrayOf(
+ MediaStore.Images.Media._ID,
+ MediaStore.Images.Media.DATE_TAKEN,
+ MediaStore.Images.Media.DATA,
+)
+
+const val IMAGE_1_ID = 1L
+const val IMAGE_2_ID = 2L
+const val IMAGE_3_ID = 3L
+const val IMAGE_4_ID = 4L
+const val IMAGE_5_ID = 5L
+
+const val TIME_2023_01_01_12_00_00 = 1672542000000 // 2023:01:01 12:00:00
+const val TIME_2024_01_01_12_00_00 = 1704078000000L // 2024:01:01 12:00:00
+const val TIME_2024_01_01_12_01_00 = 1262314860000L // 2024:01:01 12:01:00
+const val TIME_2010_01_01_12_00_00 = 1262314800000L // 2010:01:01 12:00:00
+
+val LOCATION_1 = 37.579617 to 126.977041 // 경복궁
+val LOCATION_2 = 37.2871202 to 127.0119379 // 수원 화성
+val LOCATION_3 = 35.8011781 to 128.098098 // 해인사
+
+val IMAGE_1 = arrayOf(
+ IMAGE_1_ID,
+ TIME_2023_01_01_12_00_00,
+ "PHOTO URI 1",
+)
+
+val IMAGE_2 = arrayOf(
+ IMAGE_2_ID,
+ TIME_2024_01_01_12_00_00,
+ "PHOTO URI 2",
+)
+
+val IMAGE_3_TAKEN_DATE_NULL = arrayOf(
+ IMAGE_3_ID,
+ null,
+ "PHOTO URI 3"
+)
+
+val IMAGE_4_TAKEN_DATE_NULL = arrayOf(
+ IMAGE_4_ID,
+ null,
+ "PHOTO URI 4",
+)
+val IMAGE_5 = arrayOf(
+ IMAGE_5_ID,
+ TIME_2024_01_01_12_01_00,
+ "PHOTO URI 5",
+)
+
+fun getMockUri(id: Long): Uri =
+ ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
+
+val PHOTOINFO_1 = PhotoInfo(
+ uri = getMockUri(IMAGE_1_ID),
+ latitude = LOCATION_1.first,
+ longitude = LOCATION_1.second,
+ generationTime = "2023:01:01 12:00:00"
+)
+
+val PHOTOINFO_2 = PhotoInfo(
+ uri = getMockUri(IMAGE_2_ID),
+ latitude = LOCATION_1.first,
+ longitude = LOCATION_1.second,
+ generationTime = "2024:01:01 12:00:00"
+)
+
+val PHOTOINFO_3 = PhotoInfo(
+ uri = getMockUri(IMAGE_3_ID),
+ latitude = LOCATION_2.first,
+ longitude = LOCATION_2.second,
+ generationTime = null
+)
+
+val PHOTOINFO_4 = PhotoInfo(
+ uri = getMockUri(IMAGE_4_ID),
+ latitude = LOCATION_1.first,
+ longitude = LOCATION_1.second,
+ generationTime = null
+)
+
+val PHOTOINFO_5 = PhotoInfo(
+ uri = getMockUri(IMAGE_5_ID),
+ latitude = LOCATION_2.first,
+ longitude = LOCATION_2.second,
+ generationTime = "2024:01:01 12:01:00"
+)
\ No newline at end of file
diff --git a/data/src/test/java/ny/photomap/data/PhotoDataSourceTest.kt b/data/src/test/java/ny/photomap/data/PhotoDataSourceTest.kt
new file mode 100644
index 0000000..38a1cd8
--- /dev/null
+++ b/data/src/test/java/ny/photomap/data/PhotoDataSourceTest.kt
@@ -0,0 +1,105 @@
+package ny.photomap.data
+
+import android.content.ContentResolver
+import android.database.MatrixCursor
+import android.provider.MediaStore
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.same
+import org.mockito.kotlin.whenever
+
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class PhotoDataSourceTest {
+
+ private lateinit var contentResolver: ContentResolver
+
+ @Before
+ fun setUp() {
+ contentResolver = mock()
+ }
+
+ @Test
+ fun `모든 이미지 URI 조회`() {
+ val cursor = MatrixCursor(IMAGE_COLUMNS).apply {
+ arrayOf(
+ IMAGE_1,
+ IMAGE_2,
+ IMAGE_3_TAKEN_DATE_NULL,
+ IMAGE_4_TAKEN_DATE_NULL,
+ IMAGE_5
+ ).forEach {
+ addRow(it)
+ }
+ }
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ anyOrNull(),
+ isNull(),
+ isNull(),
+ anyOrNull(),
+ isNull(),
+ )
+ ).thenReturn(cursor)
+
+
+ val list = PhotoDataSource(contentResolver).getAllPhotoUriList()
+
+ assert(list.size == 5)
+ }
+
+ @Test
+ fun `특정 기간의 이미지 URI 조회_값 있음`() {
+ val cursor = MatrixCursor(IMAGE_COLUMNS).apply {
+ arrayOf(
+ IMAGE_1,
+ IMAGE_2
+ ).forEach {
+ addRow(it)
+ }
+ }
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ anyOrNull(),
+ eq("${MediaStore.Images.Media.DATE_TAKEN} BETWEEN ? AND ?"),
+ eq(arrayOf(TIME_2010_01_01_12_00_00.toString(), TIME_2024_01_01_12_00_00.toString())),
+ anyOrNull(),
+ isNull(),
+ )
+ ).thenReturn(cursor)
+
+
+ val list = PhotoDataSource(contentResolver).getDateRangePhotoUriList(TIME_2010_01_01_12_00_00, TIME_2024_01_01_12_00_00)
+ assert(list.size == 2)
+ }
+
+ @Test
+ fun `특정 기간의 이미지 URI 조회_값 없음`() {
+
+ val cursor = MatrixCursor(IMAGE_COLUMNS)
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ anyOrNull(),
+ eq("${MediaStore.Images.Media.DATE_TAKEN} BETWEEN ? AND ?"),
+ eq(arrayOf(0.toString(), TIME_2010_01_01_12_00_00.toString())),
+ anyOrNull(),
+ isNull(),
+ )
+ ).thenReturn(cursor)
+
+ val list = PhotoDataSource(contentResolver).getDateRangePhotoUriList(0, TIME_2010_01_01_12_00_00)
+ assert(list.isEmpty())
+ }
+}
\ No newline at end of file
diff --git a/data/src/test/java/ny/photomap/data/PhotoRepositoryTest.kt b/data/src/test/java/ny/photomap/data/PhotoRepositoryTest.kt
new file mode 100644
index 0000000..bf27791
--- /dev/null
+++ b/data/src/test/java/ny/photomap/data/PhotoRepositoryTest.kt
@@ -0,0 +1,317 @@
+package ny.photomap.data
+
+import android.content.ContentResolver
+import android.database.MatrixCursor
+import android.provider.MediaStore
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.same
+import org.mockito.kotlin.whenever
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class PhotoRepositoryTest {
+
+ private lateinit var contentResolver: ContentResolver
+ private lateinit var dataSource: PhotoDataSource
+ private lateinit var repository: PhotoRepository
+
+ @Before
+ fun setUp() {
+ contentResolver = mock()
+ dataSource = Mockito.spy(PhotoDataSource(contentResolver))
+ repository = Mockito.spy(
+ PhotoRepository(
+ contentResolver = contentResolver,
+ dataSource = dataSource
+ )
+ )
+ }
+
+ @Test
+ fun `모든 사진 정보 리스트 조회`() {
+ val cursor = MatrixCursor(IMAGE_COLUMNS).apply {
+ arrayOf(
+ IMAGE_1,
+ IMAGE_2,
+ IMAGE_3_TAKEN_DATE_NULL,
+ IMAGE_4_TAKEN_DATE_NULL,
+ IMAGE_5
+ ).forEach {
+ addRow(it)
+ }
+ }
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ anyOrNull(),
+ isNull(),
+ isNull(),
+ anyOrNull(),
+ isNull(),
+ )
+ ).thenReturn(cursor)
+
+ whenever(repository.convertPhotoInfo(getMockUri(IMAGE_1_ID))).thenReturn(
+ PHOTOINFO_1
+ )
+ whenever(repository.convertPhotoInfo(getMockUri(IMAGE_2_ID))).thenReturn(
+ PHOTOINFO_2
+ )
+ whenever(repository.convertPhotoInfo(getMockUri(IMAGE_3_ID))).thenReturn(
+ PHOTOINFO_3
+ )
+ whenever(repository.convertPhotoInfo(getMockUri(IMAGE_4_ID))).thenReturn(
+ null
+ )
+ whenever(repository.convertPhotoInfo(getMockUri(IMAGE_5_ID))).thenReturn(
+ null
+ )
+
+ val list = repository.getAllPhotoInfoList()
+
+ assert(list.size == 3)
+ }
+
+ @Test
+ fun `특정 날짜 사진 정보 리스트 조회_값 있음`() {
+ val cursor = MatrixCursor(IMAGE_COLUMNS).apply {
+ arrayOf(
+ IMAGE_1,
+ IMAGE_2
+ ).forEach {
+ addRow(it)
+ }
+ }
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ anyOrNull(),
+ eq("${MediaStore.Images.Media.DATE_TAKEN} BETWEEN ? AND ?"),
+ eq(
+ arrayOf(
+ TIME_2010_01_01_12_00_00.toString(),
+ TIME_2024_01_01_12_00_00.toString()
+ )
+ ),
+ anyOrNull(),
+ isNull(),
+ )
+ ).thenReturn(cursor)
+
+ whenever(repository.convertPhotoInfo(getMockUri(IMAGE_1_ID))).thenReturn(
+ PHOTOINFO_1
+ )
+ whenever(repository.convertPhotoInfo(getMockUri(IMAGE_2_ID))).thenReturn(
+ PHOTOINFO_2
+ )
+
+ val list =
+ repository.getDateRangePhotoInfoList(TIME_2010_01_01_12_00_00, TIME_2024_01_01_12_00_00)
+
+ assert(list.size == 2)
+ }
+
+ @Test
+ fun `특정 날짜 사진 정보 리스트 조회_값 없음`() {
+ val cursor = MatrixCursor(IMAGE_COLUMNS)
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ anyOrNull(),
+ eq("${MediaStore.Images.Media.DATE_TAKEN} BETWEEN ? AND ?"),
+ eq(arrayOf(0.toString(), TIME_2010_01_01_12_00_00.toString())),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ ).thenReturn(cursor)
+
+ val list = repository.getDateRangePhotoInfoList(0, TIME_2010_01_01_12_00_00)
+ assert(list.isEmpty())
+ }
+
+ @Test
+ fun `특정 위치 사진 정보 리스트 조회_값 있음`() {
+ val cursor = MatrixCursor(IMAGE_COLUMNS).apply {
+ arrayOf(
+ IMAGE_1,
+ IMAGE_2,
+ IMAGE_3_TAKEN_DATE_NULL,
+ IMAGE_4_TAKEN_DATE_NULL,
+ IMAGE_5
+ ).forEach {
+ addRow(it)
+ }
+ }
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ anyOrNull(),
+ isNull(),
+ isNull(),
+ anyOrNull(),
+ isNull(),
+ )
+ ).thenReturn(cursor)
+
+ val surroundRange = 0.000001
+
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_1_ID),
+ targetLatitude = LOCATION_2.first,
+ targetLongitude = LOCATION_2.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_2_ID),
+ targetLatitude = LOCATION_2.first,
+ targetLongitude = LOCATION_2.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_3_ID),
+ targetLatitude = LOCATION_2.first,
+ targetLongitude = LOCATION_2.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ PHOTOINFO_3
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_4_ID),
+ targetLatitude = LOCATION_2.first,
+ targetLongitude = LOCATION_2.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_5_ID),
+ targetLatitude = LOCATION_2.first,
+ targetLongitude = LOCATION_2.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ PHOTOINFO_5
+ )
+
+ val list = repository.getLocationPhotoInfoList(
+ targetLatitude = LOCATION_2.first,
+ targetLongitude = LOCATION_2.second,
+ surroundRange = surroundRange
+ )
+
+ println("list.size: ${list.size}")
+
+ assert(list.size == 2)
+ }
+
+ @Test
+ fun `특정 위치 사진 정보 리스트 조회_값 없음`() {
+ val cursor = MatrixCursor(IMAGE_COLUMNS).apply {
+ arrayOf(
+ IMAGE_1,
+ IMAGE_2,
+ IMAGE_3_TAKEN_DATE_NULL,
+ IMAGE_4_TAKEN_DATE_NULL,
+ IMAGE_5
+ ).forEach {
+ addRow(it)
+ }
+ }
+
+ whenever(
+ contentResolver.query(
+ same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
+ isNull(),
+ isNull(),
+ isNull(),
+ anyOrNull(),
+ isNull(),
+ )
+ ).thenReturn(cursor)
+
+ val surroundRange = 0.000001
+
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_1_ID),
+ targetLatitude = LOCATION_3.first,
+ targetLongitude = LOCATION_3.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_2_ID),
+ targetLatitude = LOCATION_3.first,
+ targetLongitude = LOCATION_3.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_3_ID),
+ targetLatitude = LOCATION_3.first,
+ targetLongitude = LOCATION_3.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_4_ID),
+ targetLatitude = LOCATION_3.first,
+ targetLongitude = LOCATION_3.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+ whenever(
+ repository.convertPhotoInfo(
+ uri = getMockUri(IMAGE_5_ID),
+ targetLatitude = LOCATION_3.first,
+ targetLongitude = LOCATION_3.second,
+ surroundRange = surroundRange
+ )
+ ).thenReturn(
+ null
+ )
+
+ val list = repository.getLocationPhotoInfoList(
+ targetLatitude = LOCATION_3.first,
+ targetLongitude = LOCATION_3.second,
+ surroundRange = 0.0
+ )
+
+ assert(list.isEmpty())
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2185073..729689d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,6 +14,9 @@ activityCompose = "1.9.3"
composeBom = "2024.11.00"
secretsGradlePlugin = "2.0.1"
mapsCompose = "4.4.1"
+exifinterface = "1.3.7"
+mockito-kotlin = "5.4.0"
+robolectric = "4.14"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -38,9 +41,13 @@ secrets-gradle-plugin = { group = "com.google.android.libraries.mapsplatform.sec
maps-compose = { group = "com.google.maps.android", name = "maps-compose", version.ref = "mapsCompose" }
maps-compose-utils = { group = "com.google.maps.android", name = "maps-compose-utils", version.ref = "mapsCompose" }
maps-compose-widgets = { group = "com.google.maps.android", name = "maps-compose-widgets", version.ref = "mapsCompose" }
+androidx-exifinterface = { group="androidx.exifinterface", name= "exifinterface", version.ref = "exifinterface" }
+mockito-kotlin = { group="org.mockito.kotlin", name="mockito-kotlin", version.ref="mockito-kotlin" }
+robolectric = { group="org.robolectric", name="robolectric", version.ref="robolectric" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 61836c3..fb53de3 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -21,4 +21,4 @@ dependencyResolutionManagement {
rootProject.name = "PhotoMap"
include(":app")
-
\ No newline at end of file
+include(":data")