diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d92ad4c..bf7e595 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,8 +7,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.hilt.android) - kotlin("kapt") - + alias(libs.plugins.ksp) } android { namespace = "com.flab.deepsleep" @@ -48,9 +47,8 @@ android { } } dependencies { - kapt(libs.hilt.android.compiler) - kapt(libs.hilt.compiler) - kapt(libs.androidx.hilt.compiler) + ksp(libs.hilt.compiler) + ksp(libs.room.compiler) implementation(libs.retrofit) implementation(libs.gson) implementation(libs.hilt.android) @@ -67,6 +65,8 @@ dependencies { implementation(libs.kotlinx.coroutines.android) implementation(libs.okhttp) implementation(libs.okhttp.profiler) + implementation(libs.room.runtime) + implementation(libs.room.ktx) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) @@ -77,15 +77,6 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) } -kapt { - correctErrorTypes = true - useBuildCache = false - showProcessorStats = true - - arguments { - arg("dagger.hilt.android.internal.disableAndroidSuperclassValidation", "true") - } -} hilt { enableAggregatingTask = false -} +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/MainActivity.kt b/app/src/main/java/com/flab/deepsleep/MainActivity.kt index 1baf9fe..c4f81e2 100644 --- a/app/src/main/java/com/flab/deepsleep/MainActivity.kt +++ b/app/src/main/java/com/flab/deepsleep/MainActivity.kt @@ -2,6 +2,7 @@ package com.flab.deepsleep import PhotoAdapter import android.os.Bundle +import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog @@ -11,10 +12,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.flab.deepsleep.databinding.ActivityMainBinding import com.flab.deepsleep.ui.photo.PhotoViewModel +import com.flab.deepsleep.utils.RecyclerItemClickListener import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -26,6 +28,20 @@ class MainActivity : AppCompatActivity() { private val photoRecyclerView: RecyclerView by lazy { binding.photosRecyclerView } private val photoAdapter: PhotoAdapter by lazy { PhotoAdapter() } + private fun setRecyclerView() { + photoRecyclerView.layoutManager = GridLayoutManager(this, 2) + photoRecyclerView.adapter = photoAdapter + photoRecyclerView.addOnItemTouchListener( + RecyclerItemClickListener(this, photoRecyclerView) { _, position -> + val photo = photoAdapter.snapshot()[position] + photo?.let { + photoViewModel.insertPhoto(photo) + Toast.makeText(this, "즐겨찾기 추가", Toast.LENGTH_SHORT).show() + } + } + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -45,6 +61,7 @@ class MainActivity : AppCompatActivity() { photoViewModel.searchPhotos(text.toString()) } + /* 에러 관찰 */ photoViewModel.errorMessage.observe(this, Observer { it -> it?.let { @@ -53,11 +70,6 @@ class MainActivity : AppCompatActivity() { }) } - private fun setRecyclerView() { - photoRecyclerView.layoutManager = LinearLayoutManager(this) - photoRecyclerView.adapter = photoAdapter - } - private fun showErrorDialog(message: String) { AlertDialog.Builder(this) .setTitle("Error") @@ -67,5 +79,4 @@ class MainActivity : AppCompatActivity() { } .show() } - } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt b/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt index 6d3b78d..92833d4 100644 --- a/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt +++ b/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt @@ -2,7 +2,7 @@ package com.flab.deepsleep.data.api import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.entity.search.SearchPhotos +import com.flab.deepsleep.data.entity.photos.SearchPhotos import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query diff --git a/app/src/main/java/com/flab/deepsleep/data/di/HiltModule.kt b/app/src/main/java/com/flab/deepsleep/data/di/HiltModule.kt index d7ab1b6..5ee8f34 100644 --- a/app/src/main/java/com/flab/deepsleep/data/di/HiltModule.kt +++ b/app/src/main/java/com/flab/deepsleep/data/di/HiltModule.kt @@ -1,11 +1,16 @@ package com.flab.deepsleep.data.di +import android.content.Context import com.flab.deepsleep.BuildConfig import com.flab.deepsleep.data.api.UnplashService +import com.flab.deepsleep.data.entity.room.AppDatabase +import com.flab.deepsleep.data.repository.db.PhotoRepository +import com.flab.deepsleep.data.repository.db.OffLinePhotoRepository import com.localebro.okhttpprofiler.OkHttpProfilerInterceptor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -58,4 +63,17 @@ object HiltModule { fun provideUnplashService(retrofit: Retrofit): UnplashService { return HiltModule.retrofit.create(UnplashService::class.java) } + + /*-- Room Database --*/ + @Provides + @Singleton + fun provideItemsRepository(database: AppDatabase): PhotoRepository { + return OffLinePhotoRepository(database.photoDao()) + } + + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): AppDatabase { + return AppDatabase.getDatabase(context) + } } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/search/Results.kt b/app/src/main/java/com/flab/deepsleep/data/entity/photos/Results.kt similarity index 93% rename from app/src/main/java/com/flab/deepsleep/data/entity/search/Results.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/photos/Results.kt index ac4ef48..c4c82f0 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/search/Results.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/photos/Results.kt @@ -1,4 +1,4 @@ -package com.flab.deepsleep.data.entity.search +package com.flab.deepsleep.data.entity.photos import com.google.gson.annotations.SerializedName data class Results( diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/search/SearchPhotos.kt b/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchPhotos.kt similarity index 84% rename from app/src/main/java/com/flab/deepsleep/data/entity/search/SearchPhotos.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchPhotos.kt index 1a600d7..4b4025a 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/search/SearchPhotos.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchPhotos.kt @@ -1,4 +1,4 @@ -package com.flab.deepsleep.data.entity.search +package com.flab.deepsleep.data.entity.photos import com.google.gson.annotations.SerializedName data class SearchPhotos( diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/search/SearchUser.kt b/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchUser.kt similarity index 86% rename from app/src/main/java/com/flab/deepsleep/data/entity/search/SearchUser.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchUser.kt index bd3b062..68f5209 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/search/SearchUser.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchUser.kt @@ -1,6 +1,5 @@ -package com.flab.deepsleep.data.entity.search +package com.flab.deepsleep.data.entity.photos -import com.flab.deepsleep.data.entity.photos.Urls import com.google.gson.annotations.SerializedName data class SearchUser( diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SinglePhoto.kt b/app/src/main/java/com/flab/deepsleep/data/entity/photos/SinglePhoto.kt index 0be4f3d..2f97903 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SinglePhoto.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/photos/SinglePhoto.kt @@ -1,5 +1,6 @@ package com.flab.deepsleep.data.entity.photos +import com.flab.deepsleep.data.entity.room.Photo import com.google.gson.annotations.SerializedName data class SinglePhoto( @@ -33,24 +34,15 @@ data class SinglePhoto( val urls: Urls?, @SerializedName("user") val user: User? -){ - fun toDefault(singlePhoto: SinglePhoto): SinglePhoto{ - return SinglePhoto( - id = singlePhoto?.id, - description = singlePhoto?.description, - color = singlePhoto?.color, - createdAt = singlePhoto?.createdAt, - downloads = 0, - height = 0, - width = 0, - blurHash = singlePhoto?.blurHash, - likedByUser = false, - likes = 0, - publicDomain = true, - updatedAt = singlePhoto?.updatedAt, - exif = singlePhoto?.exif, - urls = singlePhoto?.urls, - user = singlePhoto?.user - ) - } -} \ No newline at end of file +) + +fun SinglePhoto.toPhoto(): Photo { + return Photo( + id = this.id?.toIntOrNull() ?: 0, + likes = this.likes, + urls = this.urls?.raw, + createdAt = this.createdAt, + username = this.user?.username, + isLike = true + ) +} diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/room/AppDatabase.kt b/app/src/main/java/com/flab/deepsleep/data/entity/room/AppDatabase.kt new file mode 100644 index 0000000..983943b --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/data/entity/room/AppDatabase.kt @@ -0,0 +1,24 @@ +package com.flab.deepsleep.data.entity.room + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [Photo::class], version = 2) +abstract class AppDatabase : RoomDatabase() { + abstract fun photoDao(): PhotoDao + + companion object { + @Volatile + private var Instance: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return Instance ?: synchronized(this) { + Room.databaseBuilder(context, AppDatabase::class.java, "photo_database") + .build() + .also { Instance = it } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/room/Photo.kt b/app/src/main/java/com/flab/deepsleep/data/entity/room/Photo.kt new file mode 100644 index 0000000..b44337e --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/data/entity/room/Photo.kt @@ -0,0 +1,16 @@ +package com.flab.deepsleep.data.entity.room + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "photo") +data class Photo( + @PrimaryKey(autoGenerate = true) val pk: Int = 0, + @ColumnInfo(name = "id") val id: Int, + @ColumnInfo(name = "likes") val likes: Int, + @ColumnInfo(name = "urls") val urls: String?, + @ColumnInfo(name = "created_at") val createdAt: String?, + @ColumnInfo(name = "username") val username: String?, + @ColumnInfo(name = "is_like", defaultValue = "0") val isLike: Boolean = false, +) diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/room/PhotoDao.kt b/app/src/main/java/com/flab/deepsleep/data/entity/room/PhotoDao.kt new file mode 100644 index 0000000..212a2bd --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/data/entity/room/PhotoDao.kt @@ -0,0 +1,20 @@ +package com.flab.deepsleep.data.entity.room + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface PhotoDao { + @Query("SELECT * FROM `photo` order by pk DESC") + fun getAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(vararg like: Photo) + + @Delete + suspend fun delete(like: Photo) +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repo/PhotoRepository.kt b/app/src/main/java/com/flab/deepsleep/data/repo/PhotoRepository.kt deleted file mode 100644 index 333271d..0000000 --- a/app/src/main/java/com/flab/deepsleep/data/repo/PhotoRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.flab.deepsleep.data.repo - -import com.flab.deepsleep.data.source.PhotoPagingSource - -class PhotoRepository { -// fun photoPagingSource() = PhotoPagingSource() -} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repo/UnplashRepository.kt b/app/src/main/java/com/flab/deepsleep/data/repo/UnplashRepository.kt deleted file mode 100644 index 6863ca9..0000000 --- a/app/src/main/java/com/flab/deepsleep/data/repo/UnplashRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.flab.deepsleep.data.repo - -import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.entity.search.SearchPhotos - -interface UnplashRepository { - suspend fun getRandomPhotos(count: Int) : List - suspend fun getSearchPhotos(query: String) : SearchPhotos - suspend fun getSinglePhotoById(photoId: String) : SinglePhoto -} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repository/db/OffLinePhotoRepository.kt b/app/src/main/java/com/flab/deepsleep/data/repository/db/OffLinePhotoRepository.kt new file mode 100644 index 0000000..b7d68af --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/data/repository/db/OffLinePhotoRepository.kt @@ -0,0 +1,11 @@ +package com.flab.deepsleep.data.repository.db + +import com.flab.deepsleep.data.entity.room.Photo +import com.flab.deepsleep.data.entity.room.PhotoDao +import kotlinx.coroutines.flow.Flow + +class OffLinePhotoRepository(private val photoDao: PhotoDao) : PhotoRepository { + override fun getAllPhotos(): Flow> = photoDao.getAll() + override suspend fun insertPhoto(like: Photo) = photoDao.insert(like) + override suspend fun deletePhoto(like: Photo) = photoDao.delete(like) +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repository/db/PhotoRepository.kt b/app/src/main/java/com/flab/deepsleep/data/repository/db/PhotoRepository.kt new file mode 100644 index 0000000..638f1b1 --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/data/repository/db/PhotoRepository.kt @@ -0,0 +1,10 @@ +package com.flab.deepsleep.data.repository.db + +import com.flab.deepsleep.data.entity.room.Photo +import kotlinx.coroutines.flow.Flow + +interface PhotoRepository { + fun getAllPhotos(): Flow> + suspend fun insertPhoto(like: Photo) + suspend fun deletePhoto(like: Photo) +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepository.kt b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepository.kt new file mode 100644 index 0000000..bd46239 --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepository.kt @@ -0,0 +1,10 @@ +package com.flab.deepsleep.data.repository.photo + +import com.flab.deepsleep.data.entity.photos.SinglePhoto +import com.flab.deepsleep.data.entity.photos.SearchPhotos + +interface UnplashRepository { + suspend fun getRandomPhotos(count: Int): List + suspend fun getSearchPhotos(query: String): SearchPhotos + suspend fun getSinglePhotoById(photoId: String): SinglePhoto +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repo/UnplashRepositoryImpl.kt b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepositoryImpl.kt similarity index 90% rename from app/src/main/java/com/flab/deepsleep/data/repo/UnplashRepositoryImpl.kt rename to app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepositoryImpl.kt index 1818dec..a845167 100644 --- a/app/src/main/java/com/flab/deepsleep/data/repo/UnplashRepositoryImpl.kt +++ b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepositoryImpl.kt @@ -1,9 +1,9 @@ -package com.flab.deepsleep.data.repo +package com.flab.deepsleep.data.repository.photo import com.flab.deepsleep.BuildConfig import com.flab.deepsleep.data.api.UnplashService import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.entity.search.SearchPhotos +import com.flab.deepsleep.data.entity.photos.SearchPhotos import javax.inject.Inject class UnplashRepositoryImpl @Inject constructor(private val unplashService: UnplashService) : diff --git a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt b/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt index 31aa86d..9ed9ae5 100644 --- a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt @@ -1,5 +1,4 @@ import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.paging.PagingDataAdapter @@ -8,30 +7,46 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.flab.deepsleep.R import com.flab.deepsleep.data.entity.photos.SinglePhoto +import com.flab.deepsleep.databinding.ItemPhotoBinding +import timber.log.Timber class PhotoAdapter : PagingDataAdapter(ARTICLE_DIFF_CALLBACK) { - inner class ImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val imageView: ImageView = itemView.findViewById(R.id.photoImageView) + inner class ImageViewHolder( + private val binding: ItemPhotoBinding, + ) : RecyclerView.ViewHolder(binding.root) { + val imageView: ImageView = binding.photoImageView + val btHeart: ImageView = binding.btHeart + + fun bind(photo: SinglePhoto) { + itemView.tag = photo + + /* Like Button */ + btHeart.setOnClickListener { + itemView.performClick() + } + val imageUrl = photo?.urls?.raw + if (imageUrl != null) { + Glide.with(imageView.context) + .load(imageUrl) + .placeholder(R.drawable.ic_launcher_foreground) + .into(imageView) + } else { + imageView.setImageResource(R.drawable.ic_launcher_foreground) + } + } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_photo, parent, false) - return ImageViewHolder(view) + val binding = ItemPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ImageViewHolder(binding) } override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val photo = getItem(position) - val imageUrl = photo?.urls?.raw - if (imageUrl != null) { - Glide.with(holder.imageView.context) - .load(imageUrl) - .placeholder(R.drawable.ic_launcher_foreground) - .into(holder.imageView) - } else { - holder.imageView.setImageResource(R.drawable.ic_launcher_foreground) + photo?.let { + holder.bind(photo) } } diff --git a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoViewModel.kt b/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoViewModel.kt index f6cec26..9c69ab2 100644 --- a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoViewModel.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoViewModel.kt @@ -10,10 +10,14 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.cachedIn import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.repo.UnplashRepositoryImpl +import com.flab.deepsleep.data.entity.photos.toPhoto +import com.flab.deepsleep.data.entity.room.Photo +import com.flab.deepsleep.data.repository.db.PhotoRepository +import com.flab.deepsleep.data.repository.photo.UnplashRepositoryImpl import com.flab.deepsleep.data.source.PhotoPagingSource import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -25,7 +29,8 @@ import javax.inject.Inject @HiltViewModel class PhotoViewModel @Inject constructor( - private val unplashRepository: UnplashRepositoryImpl + private val unplashRepository: UnplashRepositoryImpl, + private val photoRepository: PhotoRepository ) : ViewModel() { /* Error */ @@ -36,6 +41,9 @@ class PhotoViewModel @Inject constructor( private val _photoState = MutableLiveData?>() val photoState: MutableLiveData?> get() = _photoState + /* Photo Database */ + val allPhotos: Flow> = photoRepository.getAllPhotos() + val items: Flow> = _photoState.asFlow() .map { state -> state ?: emptyList() } .flatMapLatest { photos -> @@ -51,17 +59,29 @@ class PhotoViewModel @Inject constructor( searchDebouncer(query) } - private val searchDebouncer = debounce( - timeMillis = 300L, - coroutineScope = viewModelScope - ) { query -> - _photoState.value = null - try { - val photos = getSearchPhotos(query) - _photoState.value = photos + /* 즐겨찾기 추가 */ + fun insertPhoto(singlePhoto: SinglePhoto) { + viewModelScope.launch(Dispatchers.IO) { + photoRepository.insertPhoto(singlePhoto.toPhoto()) + } + } + + suspend fun getSearchPhotos(query: String): List? { + return try { + val searchPhotos = unplashRepository.getSearchPhotos(query) + val resultsList = searchPhotos.results?.filter { it?.description != null } + resultsList?.mapNotNull { result -> + result?.id?.let { photoId -> + try { + unplashRepository.getSinglePhotoById(photoId) + } catch (e: Exception) { + null + } + } + } } catch (e: Exception) { e.printStackTrace() - _photoState.value = emptyList() + emptyList() } } @@ -80,23 +100,17 @@ class PhotoViewModel @Inject constructor( } } - suspend fun getSearchPhotos(query: String): List? { - return try { - val searchPhotos = unplashRepository.getSearchPhotos(query) - val resultsList = searchPhotos.results?.filter { it?.description != null } - resultsList?.mapNotNull { result -> - result?.id?.let { photoId -> - try { - unplashRepository.getSinglePhotoById(photoId) - } catch (e: Exception) { - null - } - } - } + private val searchDebouncer = debounce( + timeMillis = 300L, + coroutineScope = viewModelScope + ) { query -> + _photoState.value = null + try { + val photos = getSearchPhotos(query) + _photoState.value = photos } catch (e: Exception) { e.printStackTrace() - emptyList() + _photoState.value = emptyList() } } - } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/utils/RecyclerItemClickListener.kt b/app/src/main/java/com/flab/deepsleep/utils/RecyclerItemClickListener.kt new file mode 100644 index 0000000..32f726c --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/utils/RecyclerItemClickListener.kt @@ -0,0 +1,33 @@ +package com.flab.deepsleep.utils + +import android.content.Context +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class RecyclerItemClickListener( + context: Context, + recyclerView: RecyclerView, + private val onItemClick: (View, Int) -> Unit +) : RecyclerView.OnItemTouchListener { + + private val gestureDetector = + GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(e: MotionEvent): Boolean { + return true + } + }) + + override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { + val child = rv.findChildViewUnder(e.x, e.y) + if (child != null && gestureDetector.onTouchEvent(e)) { + onItemClick(child, rv.getChildAdapterPosition(child)) + return true + } + return false + } + + override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {} + override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 0000000..74edb17 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_heart_fill.xml b/app/src/main/res/drawable/ic_heart_fill.xml new file mode 100644 index 0000000..837e2e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_fill.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 8fd8ce2..ab5b7f2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -51,4 +51,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/search_bar" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_photo.xml b/app/src/main/res/layout/item_photo.xml index 8a1e208..c3ca639 100644 --- a/app/src/main/res/layout/item_photo.xml +++ b/app/src/main/res/layout/item_photo.xml @@ -1,15 +1,50 @@ - + android:padding="10dp"> + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:layout_constraintBottom_toTopOf="@+id/photo_config_layout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a2fc41..53e662b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,5 +3,9 @@ 검색 입력 + 테스트 + + + 8dp \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 034fecf..177123c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,13 +1,11 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { kotlin("jvm") version "2.1.0" // Kotlin JVM 플러그인 - kotlin("plugin.serialization") version "2.1.0" // Serialization 플러그인 - alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.hilt.android) apply false + alias(libs.plugins.ksp) apply false } - buildscript { repositories { google() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 116f6f2..db7022a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,5 @@ [versions] agp = "8.7.3" -artifactid = "version" gson = "2.11.0" hiltAndroidCompiler = "2.51.1" kotlin = "1.9.24" @@ -14,7 +13,6 @@ activity = "1.9.3" constraintlayout = "2.2.0" retrofit = "2.9.0" hilt = "2.51.1" -hilt-lifecycle = "1.0.0-alpha03" converter = "2.9.0" viewmodel = "2.8.7" runtime = "2.8.7" @@ -24,19 +22,18 @@ glide = "4.16.0" json = "1.7.3" timber = "5.0.1" paging = "3.3.5" -kotlinx-coroutines-android = "1.3.9" +kotlinx-coroutines-android = "1.7.3" okhttp = "4.12.0" okhttp-profiler = "1.0.8" +room = "2.6.1" +ksp = "2.0.21-1.0.27" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -artifactid = { module = "groupId:artifactId", version.ref = "artifactid" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } -hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroidCompiler" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-converter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converter" } -androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt-lifecycle" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } @@ -58,8 +55,12 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } paging = { module = "androidx.paging:paging-runtime", version.ref = "paging" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-profiler = { module = "com.localebro:okhttpprofiler", version.ref = "okhttp-profiler" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } \ No newline at end of file +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file