diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a7a42e15..ebff60e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.data.video) implementation(projects.data.user) implementation(projects.data.keyword) + implementation(projects.data.exhibition) implementation(projects.local.auth) implementation(projects.local.user) implementation(projects.local.video) @@ -29,6 +30,7 @@ dependencies { implementation(projects.remote.user) implementation(projects.remote.video) implementation(projects.remote.keyword) + implementation(projects.remote.exhibition) implementation(projects.feature.navigator) implementation(libs.kakao.login) implementation(libs.hilt.androidx.common) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0164e11e..69c4d0fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,8 @@ android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" tools:ignore="ScopedStorage" /> + + Unit = {}, onBookmarkClick: () -> Unit = {}, onDeleteClick: () -> Unit = {}, + onMoreClick: () -> Unit = {}, ) { var boxSize by remember { mutableStateOf(IntSize.Zero) } var expanded by remember { mutableStateOf(false) } @@ -128,6 +129,14 @@ fun RecordyVideoText( style = RecordyTheme.typography.body2M, color = RecordyTheme.colors.gray01, ) + Spacer(modifier = Modifier.height(8.dp)) + Icon( + modifier = Modifier + .customClickable { onMoreClick() }, + painter = painterResource(id = R.drawable.ic_seemore), + contentDescription = "see more", + tint = RecordyTheme.colors.gray01, + ) Spacer(modifier = Modifier.height(if (isMyVideo) 16.dp else 20.dp)) if (isMyVideo) { Icon( diff --git a/core/designsystem/src/main/res/drawable/ic_seemore.xml b/core/designsystem/src/main/res/drawable/ic_seemore.xml new file mode 100644 index 00000000..6261dcb5 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_seemore.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_viskit_logo.xml b/core/designsystem/src/main/res/drawable/ic_viskit_logo.xml new file mode 100644 index 00000000..08e2a383 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_viskit_logo.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/core/model/src/main/java/com/record/model/VideoData.kt b/core/model/src/main/java/com/record/model/VideoData.kt index 5592e4b0..108ef3fb 100644 --- a/core/model/src/main/java/com/record/model/VideoData.kt +++ b/core/model/src/main/java/com/record/model/VideoData.kt @@ -1,12 +1,15 @@ package com.record.model data class VideoData( - val id: String, - val videoUri: String, - val previewUri: String, - val location: String, - val userName: String, - val content: String, - val bookmarkCount: Int, + val bookmarkId: Long, + val id: Long, val isBookmark: Boolean, + val bookmarkCount: Int, + val content: String, + val videoUrl: String, + val previewUrl: String, + val location: String, + val uploaderId: Long, + val nickname: String, + val isMine: Boolean, ) diff --git a/data/exhibition/.gitignore b/data/exhibition/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/exhibition/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/exhibition/build.gradle.kts b/data/exhibition/build.gradle.kts new file mode 100644 index 00000000..a2b44962 --- /dev/null +++ b/data/exhibition/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.recordy.data) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.record.exhibition" +} + +dependencies { + implementation(projects.domain.exhibition) + implementation(projects.domain.video) +} diff --git a/data/exhibition/src/main/AndroidManifest.xml b/data/exhibition/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/data/exhibition/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/data/exhibition/src/main/java/com/example/exhibition/di/RepositoryModule.kt b/data/exhibition/src/main/java/com/example/exhibition/di/RepositoryModule.kt new file mode 100644 index 00000000..9e36ff32 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/di/RepositoryModule.kt @@ -0,0 +1,23 @@ +package com.example.exhibition.di + +import com.example.exhibition.repository.ExhibitionRepositoryImpl +import com.example.exhibition.repository.SearchRepositoryImpl +import com.record.exhibition.repository.ExhibitionRepository +import com.record.exhibition.repository.SearchRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + @Singleton + abstract fun bindsExhibitionRepository(exhibitionRepositoryImpl: ExhibitionRepositoryImpl): ExhibitionRepository + + @Binds + @Singleton + abstract fun bindsSearchRepository(searchRepositoryImpl: SearchRepositoryImpl): SearchRepository +} diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPatchExhibitionDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPatchExhibitionDto.kt new file mode 100644 index 00000000..49a2ff5e --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPatchExhibitionDto.kt @@ -0,0 +1,18 @@ +package com.example.exhibition.model.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPatchExhibitionDto( + @SerialName("endDate") + val endDate: String, + @SerialName("id") + val id: Int, + @SerialName("isFree") + val isFree: Boolean, + @SerialName("name") + val name: String, + @SerialName("startDate") + val startDate: String, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPostExhibitionDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPostExhibitionDto.kt new file mode 100644 index 00000000..e4e86f69 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPostExhibitionDto.kt @@ -0,0 +1,18 @@ +package com.example.exhibition.model.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPostExhibitionDto( + @SerialName("endDate") + val endDate: String, + @SerialName("isFree") + val isFree: Boolean, + @SerialName("name") + val name: String, + @SerialName("placeId") + val placeId: Int, + @SerialName("startDate") + val startDate: String, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPostPlaceDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPostPlaceDto.kt new file mode 100644 index 00000000..04bcc883 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/request/RequestPostPlaceDto.kt @@ -0,0 +1,18 @@ +package com.example.exhibition.model.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPostPlaceDto( + @SerialName("address") + val address: String, + @SerialName("id") + val id: String, + @SerialName("latitude") + val latitude: Int, + @SerialName("longitude") + val longitude: Int, + @SerialName("name") + val name: String, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/Location.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/Location.kt new file mode 100644 index 00000000..6d140ca2 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/Location.kt @@ -0,0 +1,14 @@ +package com.example.exhibition.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Location( + @SerialName("id") + val id: Int, + @SerialName("latitude") + val latitude: Double, + @SerialName("longitude") + val longitude: Double, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetExhibitionSearchDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetExhibitionSearchDto.kt new file mode 100644 index 00000000..fe09f28e --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetExhibitionSearchDto.kt @@ -0,0 +1,29 @@ +package com.example.exhibition.model.remote.response + +import com.record.exhibition.model.ResultType +import com.record.exhibition.model.SearchResult +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseGetExhibitionSearchDto( + @SerialName("id") + val id: Long, + @SerialName("type") + val type: String, + @SerialName("address") + val address: String, + @SerialName("name") + val name: String, +) + +fun ResponseGetExhibitionSearchDto.toDomain() = SearchResult( + id = this.id, + type = when (this.type) { + "PLACE" -> ResultType.PLACE + "EXHIBITION" -> ResultType.EXHIBITION + else -> ResultType.UNKNOWN + }, + address = this.address, + name = this.name, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetExhibitionsDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetExhibitionsDto.kt new file mode 100644 index 00000000..28c4fc82 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetExhibitionsDto.kt @@ -0,0 +1,27 @@ +package com.example.exhibition.model.remote.response + +import com.record.exhibition.model.Exhibition +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseGetExhibitionsDto( + @SerialName("id") + val id: Int, + @SerialName("isFree") + val isFree: Boolean, + @SerialName("name") + val name: String, + @SerialName("startDate") + val startDate: String, + @SerialName("endDate") + val endDate: String, +) + +fun ResponseGetExhibitionsDto.toDomain() = Exhibition( + id = this.id, + isFree = this.isFree, + name = this.name, + startDate = this.startDate, + endDate = this.endDate, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetPagingPlaceDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetPagingPlaceDto.kt new file mode 100644 index 00000000..10a9c874 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetPagingPlaceDto.kt @@ -0,0 +1,14 @@ +package com.example.exhibition.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseGetPagingPlaceDto( + @SerialName("content") + val content: List, + @SerialName("hasNext") + val hasNext: Boolean, + @SerialName("pageNumber") + val pageNumber: Int, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetPlaceDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetPlaceDto.kt new file mode 100644 index 00000000..fad8868a --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetPlaceDto.kt @@ -0,0 +1,22 @@ +package com.example.exhibition.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseGetPlaceDto( + @SerialName("address") + val address: String?, + @SerialName("exhibitionSize") + val exhibitionSize: Int, + @SerialName("id") + val id: Int, + @SerialName("location") + val location: Location, + @SerialName("name") + val name: String, + @SerialName("platformId") + val platformId: String?, + @SerialName("recordSize") + val recordSize: Int, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetReviewsDto.kt b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetReviewsDto.kt new file mode 100644 index 00000000..36c7ee95 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/model/remote/response/ResponseGetReviewsDto.kt @@ -0,0 +1,18 @@ +package com.example.exhibition.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseGetReviewsDto( + @SerialName("authorName") + val authorName: String, + @SerialName("content") + val content: String, + @SerialName("createdAt") + val createdAt: String, + @SerialName("id") + val id: Int, + @SerialName("rating") + val rating: Int, +) diff --git a/data/exhibition/src/main/java/com/example/exhibition/repository/ExhibitionRepositoryImpl.kt b/data/exhibition/src/main/java/com/example/exhibition/repository/ExhibitionRepositoryImpl.kt new file mode 100644 index 00000000..dc4f221c --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/repository/ExhibitionRepositoryImpl.kt @@ -0,0 +1,96 @@ +package com.example.exhibition.repository + +import com.example.exhibition.model.remote.response.toDomain +import com.example.exhibition.source.remote.RemoteExhibitionDataSource +import com.example.exhibition.source.remote.RemotePlaceDataSource +import com.record.exhibition.model.Exhibition +import com.record.exhibition.model.ExhibitionFilter +import com.record.exhibition.model.Place +import com.record.exhibition.repository.ExhibitionRepository +import com.record.model.Page +import com.record.model.exception.ApiError +import com.record.video.model.toCore +import com.record.video.repository.VideoRepository +import retrofit2.HttpException +import javax.inject.Inject + +class ExhibitionRepositoryImpl @Inject constructor( + private val remoteExhibitionDataSource: RemoteExhibitionDataSource, + private val remotePlaceDataSource: RemotePlaceDataSource, + private val videoRepository: VideoRepository, +) : ExhibitionRepository { + override suspend fun getNearPlaceData(number: Int, size: Int, latitude: Double, longitude: Double) = + runCatching { + remotePlaceDataSource.getNearPlace(number = number, size = size, latitude = latitude, longitude = -longitude, distance = 3000000.0) + }.mapCatching { it -> + Page( + hasNext = it.hasNext, + page = it.pageNumber, + data = it.content.map { + val result = videoRepository.getPlaceVideos(it.id, 0, it.recordSize).getOrNull() + Place( + placeId = it.id, + address = it.address ?: "", + name = it.name, + exhibitionCount = it.exhibitionSize, + recordCount = it.recordSize, + exhibitionRecord = result?.data?.map { it.toCore() }, + ) + }, + ) + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + throw ApiError(exception.message()) + } + + else -> { + throw exception + } + } + } + + override suspend fun getPlaceById(placeId: Long): Result = runCatching { + remotePlaceDataSource.getPlaceById(placeId.toInt()) + }.mapCatching { it -> + val result = videoRepository.getPlaceVideos(it.id, 0, it.recordSize).getOrNull() + Place( + placeId = it.id, + address = it.address ?: "", + name = it.name, + exhibitionCount = it.exhibitionSize, + recordCount = it.recordSize, + exhibitionRecord = result?.data?.map { it.toCore() }, + ) + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + throw ApiError(exception.message()) + } + + else -> { + throw exception + } + } + } + + override suspend fun getExhibitions(placeId: Long, filter: ExhibitionFilter): Result> = runCatching { + when (filter) { + ExhibitionFilter.DEFAULT -> remoteExhibitionDataSource.getExhibitionById(placeId.toInt()) + ExhibitionFilter.FREE -> remoteExhibitionDataSource.getFreeExhibition(placeId.toInt()) + ExhibitionFilter.CLOSING -> remoteExhibitionDataSource.getClosingExhibition(placeId.toInt()) + } + }.mapCatching { + it.map { it.toDomain() } + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + throw ApiError(exception.message()) + } + + else -> { + throw exception + } + } + } +} diff --git a/data/exhibition/src/main/java/com/example/exhibition/repository/SearchRepositoryImpl.kt b/data/exhibition/src/main/java/com/example/exhibition/repository/SearchRepositoryImpl.kt new file mode 100644 index 00000000..fce9965a --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/repository/SearchRepositoryImpl.kt @@ -0,0 +1,29 @@ +package com.example.exhibition.repository + +import com.example.exhibition.model.remote.response.toDomain +import com.example.exhibition.source.remote.RemoteSearchDataSource +import com.record.exhibition.model.SearchResult +import com.record.exhibition.repository.SearchRepository +import com.record.model.exception.ApiError +import retrofit2.HttpException +import javax.inject.Inject + +class SearchRepositoryImpl @Inject constructor( + private val searchDataSource: RemoteSearchDataSource, +) : SearchRepository { + override suspend fun searchExhibition(query: String): Result> = runCatching { + searchDataSource.searchExhibition(query) + }.mapCatching { + it.map { it.toDomain() } + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + throw ApiError(exception.message()) + } + + else -> { + throw exception + } + } + } +} diff --git a/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemoteExhibitionDataSource.kt b/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemoteExhibitionDataSource.kt new file mode 100644 index 00000000..53c656f1 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemoteExhibitionDataSource.kt @@ -0,0 +1,31 @@ +package com.example.exhibition.source.remote + +import com.example.exhibition.model.remote.request.RequestPatchExhibitionDto +import com.example.exhibition.model.remote.request.RequestPostExhibitionDto +import com.example.exhibition.model.remote.response.ResponseGetExhibitionsDto + +interface RemoteExhibitionDataSource { + suspend fun postExhibition( + requestPostExhibitionDto: RequestPostExhibitionDto, + ) + + suspend fun getExhibitionById( + placeId: Int, + ): List + + suspend fun getFreeExhibition( + placeId: Int, + ): List + + suspend fun getClosingExhibition( + placeId: Int, + ): List + + suspend fun patchExhibition( + requestPatchExhibitionDto: RequestPatchExhibitionDto, + ) + + suspend fun deleteExhibition( + exhibitionId: Int, + ) +} diff --git a/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemotePlaceDataSource.kt b/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemotePlaceDataSource.kt new file mode 100644 index 00000000..0e6e17ce --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemotePlaceDataSource.kt @@ -0,0 +1,33 @@ +package com.example.exhibition.source.remote + +import com.example.exhibition.model.remote.request.RequestPostPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetPagingPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetReviewsDto + +interface RemotePlaceDataSource { + suspend fun postPlace( + requestPostPlaceDto: RequestPostPlaceDto, + ) + + suspend fun getPlaceById( + id: Int, + ): ResponseGetPlaceDto + + suspend fun getReviewsById( + id: Int, + ): List + + suspend fun getNearPlace( + number: Int, + size: Int, + latitude: Double, + longitude: Double, + distance: Double, + ): ResponseGetPagingPlaceDto + + suspend fun getHasInProgressExhibitionPlaces( + number: Int, + size: Int, + ): List +} diff --git a/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemoteSearchDataSource.kt b/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemoteSearchDataSource.kt new file mode 100644 index 00000000..0c4ab239 --- /dev/null +++ b/data/exhibition/src/main/java/com/example/exhibition/source/remote/RemoteSearchDataSource.kt @@ -0,0 +1,7 @@ +package com.example.exhibition.source.remote + +import com.example.exhibition.model.remote.response.ResponseGetExhibitionSearchDto + +interface RemoteSearchDataSource { + suspend fun searchExhibition(query: String): List +} diff --git a/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt b/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt index 5147a3e4..903d1c75 100644 --- a/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt +++ b/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt @@ -66,6 +66,22 @@ class VideoRepositoryImpl @Inject constructor( } } + override suspend fun getPlaceVideos(placeId: Int, cursor: Long, pageSize: Int): Result> = runCatching { + remoteVideoDataSource.getPlaceVideos(placeId, cursor, pageSize) + }.mapCatching { + it.toCore() + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + throw ApiError(exception.message()) + } + + else -> { + throw exception + } + } + } + override suspend fun getUserVideos(otherUserId: Long, cursorId: Long, size: Int): Result> = runCatching { remoteVideoDataSource.getUserVideos(otherUserId, cursorId, size) }.mapCatching { diff --git a/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt b/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt index 073e7ca3..758adb72 100644 --- a/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt +++ b/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt @@ -9,6 +9,7 @@ interface RemoteVideoDataSource { suspend fun getAllVideos(cursorId: Long, size: Int): List suspend fun getRecentVideos(keywords: List?, cursor: Long, pageSize: Int): ResponseGetSliceVideoDto suspend fun getPopularVideos(keywords: List?, pageNumber: Int, pageSize: Int): ResponseGetPagingVideoDto + suspend fun getPlaceVideos(placeId: Int, cursor: Long, pageSize: Int): ResponseGetSliceVideoDto suspend fun getUserVideos(otherUserId: Long, cursorId: Long, size: Int): ResponseGetSliceVideoDto suspend fun getFollowingVideos(cursorId: Long, size: Int): ResponseGetSliceVideoDto suspend fun getBookmarkVideos(cursorId: Long, size: Int): ResponseGetBookmarkSliceVideoDto diff --git a/domain/exhibition/.gitignore b/domain/exhibition/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/exhibition/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/exhibition/build.gradle.kts b/domain/exhibition/build.gradle.kts new file mode 100644 index 00000000..8c84885d --- /dev/null +++ b/domain/exhibition/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + alias(libs.plugins.recordy.java.library) +} + +dependencies { + implementation(projects.core.model) + implementation(libs.kotlinx.coroutines.core) +} diff --git a/domain/exhibition/src/main/java/com/record/exhibition/model/Exhibition.kt b/domain/exhibition/src/main/java/com/record/exhibition/model/Exhibition.kt new file mode 100644 index 00000000..bdfb981e --- /dev/null +++ b/domain/exhibition/src/main/java/com/record/exhibition/model/Exhibition.kt @@ -0,0 +1,9 @@ +package com.record.exhibition.model + +data class Exhibition( + val id: Int, + val isFree: Boolean, + val name: String, + val startDate: String, + val endDate: String, +) diff --git a/domain/exhibition/src/main/java/com/record/exhibition/model/ExhibitionFilter.kt b/domain/exhibition/src/main/java/com/record/exhibition/model/ExhibitionFilter.kt new file mode 100644 index 00000000..6b5b2deb --- /dev/null +++ b/domain/exhibition/src/main/java/com/record/exhibition/model/ExhibitionFilter.kt @@ -0,0 +1,7 @@ +package com.record.exhibition.model + +enum class ExhibitionFilter { + DEFAULT, + FREE, + CLOSING, +} diff --git a/domain/exhibition/src/main/java/com/record/exhibition/model/Place.kt b/domain/exhibition/src/main/java/com/record/exhibition/model/Place.kt new file mode 100644 index 00000000..4759bc95 --- /dev/null +++ b/domain/exhibition/src/main/java/com/record/exhibition/model/Place.kt @@ -0,0 +1,12 @@ +package com.record.exhibition.model + +import com.record.model.VideoData + +data class Place( + val placeId: Int, + val address: String, + val name: String, + val exhibitionCount: Int, + val recordCount: Int, + val exhibitionRecord: List?, +) diff --git a/domain/exhibition/src/main/java/com/record/exhibition/model/ResultType.kt b/domain/exhibition/src/main/java/com/record/exhibition/model/ResultType.kt new file mode 100644 index 00000000..a83a2608 --- /dev/null +++ b/domain/exhibition/src/main/java/com/record/exhibition/model/ResultType.kt @@ -0,0 +1,7 @@ +package com.record.exhibition.model + +enum class ResultType { + PLACE, + EXHIBITION, + UNKNOWN, +} diff --git a/domain/exhibition/src/main/java/com/record/exhibition/model/SearchResult.kt b/domain/exhibition/src/main/java/com/record/exhibition/model/SearchResult.kt new file mode 100644 index 00000000..c371d47d --- /dev/null +++ b/domain/exhibition/src/main/java/com/record/exhibition/model/SearchResult.kt @@ -0,0 +1,8 @@ +package com.record.exhibition.model + +data class SearchResult( + val id: Long, + val type: ResultType, + val address: String, + val name: String, +) diff --git a/domain/exhibition/src/main/java/com/record/exhibition/repository/ExhibitionRepository.kt b/domain/exhibition/src/main/java/com/record/exhibition/repository/ExhibitionRepository.kt new file mode 100644 index 00000000..0acecdf0 --- /dev/null +++ b/domain/exhibition/src/main/java/com/record/exhibition/repository/ExhibitionRepository.kt @@ -0,0 +1,12 @@ +package com.record.exhibition.repository + +import com.record.exhibition.model.Exhibition +import com.record.exhibition.model.ExhibitionFilter +import com.record.exhibition.model.Place +import com.record.model.Page + +interface ExhibitionRepository { + suspend fun getNearPlaceData(number: Int, size: Int, latitude: Double, longitude: Double): Result> + suspend fun getPlaceById(placeId: Long): Result + suspend fun getExhibitions(placeId: Long, filter: ExhibitionFilter): Result> +} diff --git a/domain/exhibition/src/main/java/com/record/exhibition/repository/SearchRepository.kt b/domain/exhibition/src/main/java/com/record/exhibition/repository/SearchRepository.kt new file mode 100644 index 00000000..1138c604 --- /dev/null +++ b/domain/exhibition/src/main/java/com/record/exhibition/repository/SearchRepository.kt @@ -0,0 +1,7 @@ +package com.record.exhibition.repository + +import com.record.exhibition.model.SearchResult + +interface SearchRepository { + suspend fun searchExhibition(query: String): Result> +} diff --git a/domain/video/src/main/java/com/record/video/model/VideoData.kt b/domain/video/src/main/java/com/record/video/model/VideoData.kt index be87793e..6060c954 100644 --- a/domain/video/src/main/java/com/record/video/model/VideoData.kt +++ b/domain/video/src/main/java/com/record/video/model/VideoData.kt @@ -13,3 +13,17 @@ data class VideoData( val nickname: String, val isMine: Boolean, ) + +fun VideoData.toCore() = com.record.model.VideoData( + bookmarkCount = this.bookmarkCount, + id = this.id, + isBookmark = this.isBookmark, + bookmarkId = this.bookmarkId, + content = this.content, + videoUrl = this.videoUrl, + previewUrl = this.previewUrl, + location = this.location, + uploaderId = this.uploaderId, + nickname = this.nickname, + isMine = this.isMine, +) diff --git a/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt b/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt index 4fe1c8ca..c597fcb4 100644 --- a/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt +++ b/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt @@ -8,6 +8,7 @@ interface VideoRepository { suspend fun getAllVideos(cursorId: Long, pageSize: Int): Result> suspend fun getRecentVideos(keywords: List?, cursor: Long, pageSize: Int): Result> suspend fun getPopularVideos(keywords: List?, pageNumber: Int, pageSize: Int): Result> + suspend fun getPlaceVideos(placeId: Int, cursor: Long, pageSize: Int): Result> suspend fun getUserVideos(otherUserId: Long, cursorId: Long, size: Int): Result> suspend fun getMyVideos(cursorId: Long, size: Int): Result> suspend fun getFollowingVideos(cursorId: Long, size: Int): Result> diff --git a/feature/detail/build.gradle.kts b/feature/detail/build.gradle.kts index 7c41f5eb..bf9b56c1 100644 --- a/feature/detail/build.gradle.kts +++ b/feature/detail/build.gradle.kts @@ -7,4 +7,5 @@ android { } dependencies { implementation(projects.domain.video) + implementation(projects.domain.exhibition) } diff --git a/feature/detail/src/main/java/com/record/detail/DetailScreen.kt b/feature/detail/src/main/java/com/record/detail/DetailScreen.kt index e6909a40..43b7c0ad 100644 --- a/feature/detail/src/main/java/com/record/detail/DetailScreen.kt +++ b/feature/detail/src/main/java/com/record/detail/DetailScreen.kt @@ -53,7 +53,7 @@ fun DetailRoute( padding: PaddingValues, modifier: Modifier = Modifier, viewModel: DetailpageViewModel = hiltViewModel(), - navigateToUplaod: () -> Unit, + navigateToUpload: () -> Unit, navigateToVideo: (VideoType, Long) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -82,7 +82,8 @@ fun DetailRoute( navigateToVideo = viewModel::navigateToVideoDetail, onLoadMoreReviews = viewModel::loadMoreReviewVideos, onBookmarkClick = viewModel::bookmark, - navigateToUpload = navigateToUplaod, + navigateToUpload = navigateToUpload, + onChipSelected = viewModel::selectChip, ) } } @@ -96,6 +97,7 @@ fun DetailpageScreen( navigateToUpload: () -> Unit, onLoadMoreReviews: () -> Unit, onBookmarkClick: (Long) -> Unit, + onChipSelected: (ChipTab) -> Unit, ) { val pagerState = rememberPagerState( initialPage = state.detailpageTab.ordinal, @@ -178,10 +180,10 @@ fun DetailpageScreen( ListScreen( exhibitionItems = state.exhibitionList, exhibitionCount = state.exhibitionCount, - selectedChip = selectedChipState.value, + selectedChip = state.selectedChip, onItemClick = {}, onChipSelected = { selectedChip -> - selectedChipState.value = selectedChip + onChipSelected(selectedChip) }, ) } @@ -220,7 +222,7 @@ fun CustomTabRow( val animatedIndicatorWidth by animateDpAsState( targetValue = tabWidth - 12.dp, - animationSpec = tween(200), + animationSpec = tween(0), ) val density = LocalDensity.current diff --git a/feature/detail/src/main/java/com/record/detail/DetailpageContract.kt b/feature/detail/src/main/java/com/record/detail/DetailpageContract.kt index 0a80d45d..bb2ec294 100644 --- a/feature/detail/src/main/java/com/record/detail/DetailpageContract.kt +++ b/feature/detail/src/main/java/com/record/detail/DetailpageContract.kt @@ -1,21 +1,25 @@ package com.record.detail +import com.record.detail.screen.ChipTab +import com.record.exhibition.model.Exhibition +import com.record.model.VideoData import com.record.model.VideoType import com.record.ui.base.SideEffect import com.record.ui.base.UiState -import com.record.video.model.VideoData import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList data class DetailpageState( - val placeName: String = "국립현대미술관", - val placeAddress: String = "서울특별시 종로구 삼청로 30", + val placeId: Long = 0, + val placeName: String = "", + val placeAddress: String = "", val exhibitionCount: Int = 0, val reviewVideoCount: Int = 0, val reviewCursor: Long = 0, val reviewIsEnd: Boolean = false, val detailpageTab: DetailpageTab = DetailpageTab.LIST, - val exhibitionList: ImmutableList> = emptyList>().toImmutableList(), + val selectedChip: ChipTab = ChipTab.ALL, + val exhibitionList: ImmutableList = emptyList().toImmutableList(), val reviewList: ImmutableList = emptyList().toImmutableList(), ) : UiState diff --git a/feature/detail/src/main/java/com/record/detail/DetailpageViewModel.kt b/feature/detail/src/main/java/com/record/detail/DetailpageViewModel.kt index c016d53c..c874bc72 100644 --- a/feature/detail/src/main/java/com/record/detail/DetailpageViewModel.kt +++ b/feature/detail/src/main/java/com/record/detail/DetailpageViewModel.kt @@ -1,8 +1,16 @@ package com.record.detail +import android.util.Log +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.record.detail.navigation.DetailRoute +import com.record.detail.screen.ChipTab +import com.record.exhibition.model.ExhibitionFilter +import com.record.exhibition.repository.ExhibitionRepository +import com.record.model.VideoData import com.record.model.VideoType import com.record.ui.base.BaseViewModel +import com.record.video.model.toCore import com.record.video.repository.VideoRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList @@ -13,19 +21,64 @@ import javax.inject.Inject @HiltViewModel class DetailpageViewModel @Inject constructor( private val videoRepository: VideoRepository, + private val exhibitionRepository: ExhibitionRepository, + savedStateHandle: SavedStateHandle, ) : BaseViewModel(DetailpageState()) { + private val placeIdString = savedStateHandle.get(DetailRoute.PLACE_ID) + init { + intent { + copy(placeId = placeIdString?.toLong() ?: 0) + } + selectChip(uiState.value.selectedChip) + } fun selectTab(tab: DetailpageTab) { intent { copy(detailpageTab = tab) } } + fun selectChip(chip: ChipTab) { + intent { + copy(selectedChip = chip) + } + viewModelScope.launch { + exhibitionRepository.getExhibitions( + placeId = uiState.value.placeId, + filter = when (uiState.value.selectedChip) { + ChipTab.ALL -> ExhibitionFilter.DEFAULT + ChipTab.FREE -> ExhibitionFilter.FREE + ChipTab.ENDING_SOON -> ExhibitionFilter.CLOSING + }, + ).onSuccess { + intent { + copy( + exhibitionList = it.toImmutableList(), + ) + } + }.onFailure { + Log.e("이잉", it.message.toString()) + } + } + } + fun navigateToVideoDetail(type: VideoType, videoId: Long) { postSideEffect(DetailpageSideEffect.NavigateToVideoDetail(type, videoId)) } - fun fetchPlaceInfo() { + fun fetchPlaceInfo() = viewModelScope.launch { + exhibitionRepository.getPlaceById(uiState.value.placeId).onSuccess { + intent { + copy( + placeAddress = it.address, + placeName = it.name, + exhibitionCount = it.exhibitionCount, + reviewVideoCount = it.recordCount, + reviewList = it.exhibitionRecord?.toImmutableList() ?: emptyList().toImmutableList(), + ) + } + }.onFailure { + } } fun initialData() = viewModelScope.launch { @@ -39,7 +92,7 @@ class DetailpageViewModel @Inject constructor( val reviewVideo = reviewRes.getOrThrow() intent { copy( - reviewList = reviewVideo.data.toImmutableList(), + reviewList = reviewVideo.data.map { it.toCore() }.toImmutableList(), reviewCursor = reviewVideo.nextCursor?.toLong() ?: 0, reviewIsEnd = false, ) @@ -54,7 +107,7 @@ class DetailpageViewModel @Inject constructor( intent { copy( reviewCursor = it.nextCursor?.toLong() ?: 0, - reviewList = (list + it.data).toImmutableList(), + reviewList = (list + it.data.map { it.toCore() }).toImmutableList(), ) } if (!it.hasNext) { diff --git a/feature/detail/src/main/java/com/record/detail/navigation/DetailNavigation.kt b/feature/detail/src/main/java/com/record/detail/navigation/DetailNavigation.kt index ed2fc4e5..52c44b33 100644 --- a/feature/detail/src/main/java/com/record/detail/navigation/DetailNavigation.kt +++ b/feature/detail/src/main/java/com/record/detail/navigation/DetailNavigation.kt @@ -7,10 +7,11 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.record.detail.DetailRoute +import com.record.detail.navigation.DetailRoute.PLACE_ID import com.record.model.VideoType -fun NavController.navigateDetail(navOptions: NavOptions) { - navigate(DetailRoute.route, navOptions) +fun NavController.navigateDetail(placeId: Long, navOptions: NavOptions) { + navigate(DetailRoute.detailRoute(placeId.toString()), navOptions) } fun NavGraphBuilder.detailNavGraph( @@ -19,16 +20,22 @@ fun NavGraphBuilder.detailNavGraph( navigateToUpload: () -> Unit, navigateToVideo: (VideoType, Long) -> Unit, ) { - composable(route = DetailRoute.route) { + composable( + route = DetailRoute.detailRoute( + "{$PLACE_ID}", + ), + ) { DetailRoute( padding = padding, modifier = modifier, navigateToVideo = navigateToVideo, - navigateToUplaod = navigateToUpload, + navigateToUpload = navigateToUpload, ) } } object DetailRoute { - const val route = "search" + const val route = "place-detail" + const val PLACE_ID = "place-id" + fun detailRoute(placeId: String) = "$route/$placeId" } diff --git a/feature/detail/src/main/java/com/record/detail/screen/ListScreen.kt b/feature/detail/src/main/java/com/record/detail/screen/ListScreen.kt index e0377e89..3357022d 100644 --- a/feature/detail/src/main/java/com/record/detail/screen/ListScreen.kt +++ b/feature/detail/src/main/java/com/record/detail/screen/ListScreen.kt @@ -25,11 +25,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.record.designsystem.R import com.record.designsystem.theme.RecordyTheme +import com.record.exhibition.model.Exhibition import kotlinx.collections.immutable.ImmutableList @Composable fun ListScreen( - exhibitionItems: ImmutableList>, + exhibitionItems: ImmutableList, exhibitionCount: Int, selectedChip: ChipTab, onChipSelected: (ChipTab) -> Unit, @@ -74,7 +75,7 @@ fun ListScreen( } } item { Spacer(modifier = Modifier.height(16.dp)) } - if (exhibitionCount == 0) { + if (exhibitionItems.size == 0) { item { EmptyDataScreen( message = "\n진행 중인 전시가 없어요.", @@ -83,16 +84,13 @@ fun ListScreen( } } else { items(exhibitionItems) { item -> - val (name, startDate, endDate) = item - Column { - ExhibitionItem( - name = name, - startDate = startDate, - endDate = endDate, - onButtonClick = { }, - ) - Spacer(modifier = Modifier.height(16.dp)) - } + ExhibitionItem( + name = item.name, + startDate = item.startDate, + endDate = item.endDate, + onButtonClick = { }, + ) + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/feature/detail/src/main/java/com/record/detail/screen/ReviewScreen.kt b/feature/detail/src/main/java/com/record/detail/screen/ReviewScreen.kt index 1fe067df..37fa6541 100644 --- a/feature/detail/src/main/java/com/record/detail/screen/ReviewScreen.kt +++ b/feature/detail/src/main/java/com/record/detail/screen/ReviewScreen.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.record.designsystem.component.RecordyVideoThumbnail import com.record.designsystem.theme.RecordyTheme +import com.record.model.VideoData import com.record.model.VideoType import com.record.ui.scroll.OnBottomReached -import com.record.video.model.VideoData import kotlinx.collections.immutable.ImmutableList @Composable diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 141c606f..94ded6cb 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -9,6 +9,8 @@ android { dependencies { implementation(projects.domain.video) implementation(projects.domain.keyword) + implementation(projects.domain.exhibition) implementation(libs.lottie.compose) implementation(libs.collapsing.toolbar) + implementation(libs.google.location) } diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml index a5918e68..769b825c 100644 --- a/feature/home/src/main/AndroidManifest.xml +++ b/feature/home/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - \ No newline at end of file + + diff --git a/feature/home/src/main/java/com/record/home/Exhibition.kt b/feature/home/src/main/java/com/record/home/Exhibition.kt new file mode 100644 index 00000000..c1378fa2 --- /dev/null +++ b/feature/home/src/main/java/com/record/home/Exhibition.kt @@ -0,0 +1,12 @@ +package com.record.home + +import com.record.video.model.VideoData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +data class Exhibition( + val location: String, + val name: String, + val exhibitionCount: Int, + val userVideo: ImmutableList = emptyList().toImmutableList(), +) diff --git a/feature/home/src/main/java/com/record/home/HomeContract.kt b/feature/home/src/main/java/com/record/home/HomeContract.kt index 5da09f9c..f6731c58 100644 --- a/feature/home/src/main/java/com/record/home/HomeContract.kt +++ b/feature/home/src/main/java/com/record/home/HomeContract.kt @@ -1,22 +1,23 @@ package com.record.home -import com.record.model.VideoType +import com.record.exhibition.model.Place import com.record.ui.base.SideEffect import com.record.ui.base.UiState -import com.record.video.model.VideoData import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList data class HomeState( - val chipList: ImmutableList = listOf("전체").toImmutableList(), - val popularList: ImmutableList = emptyList().toImmutableList(), - val recentList: ImmutableList = emptyList().toImmutableList(), - val selectedChipIndex: Int? = 0, + val exhibitionList: ImmutableList = emptyList().toImmutableList(), val isLoading: Boolean = false, + val location: Location = Location(0.0, 0.0), + val page: Int = 0, + val isEnd: Boolean = false, + val showLocationPermissionDialog: Boolean = true, ) : UiState sealed interface HomeSideEffect : SideEffect { data object navigateToUpload : HomeSideEffect - data class navigateToVideo(val id: Long, val type: VideoType, val keyword: String?) : HomeSideEffect - data object collapseToolbar : HomeSideEffect + data class navigateToVideo(val id: Long, val location: String) : HomeSideEffect + data class navigateToDetail(val id: Long) : HomeSideEffect + data object launchSettingIntent : HomeSideEffect } diff --git a/feature/home/src/main/java/com/record/home/HomeScreen.kt b/feature/home/src/main/java/com/record/home/HomeScreen.kt index c4e509b9..723a1478 100644 --- a/feature/home/src/main/java/com/record/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/record/home/HomeScreen.kt @@ -1,91 +1,78 @@ package com.record.home -import androidx.compose.foundation.Image +import android.Manifest +import android.content.pm.PackageManager +import android.location.Location +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.LottieConstants -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices import com.record.designsystem.R import com.record.designsystem.component.RecordyVideoThumbnail -import com.record.designsystem.component.button.RecordyChipButton +import com.record.designsystem.component.dialog.RecordyDialog import com.record.designsystem.theme.RecordyTheme -import com.record.home.component.UploadFloatingButton +import com.record.exhibition.model.Place import com.record.model.VideoType -import com.record.ui.extension.customClickable import com.record.ui.lifecycle.LaunchedEffectWithLifecycle -import com.record.video.model.VideoData +import com.record.ui.scroll.OnBottomReached +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import me.onebone.toolbar.CollapsingToolbarScaffold -import me.onebone.toolbar.CollapsingToolbarScaffoldState -import me.onebone.toolbar.CollapsingToolbarScope -import me.onebone.toolbar.ExperimentalToolbarApi -import me.onebone.toolbar.ScrollStrategy -import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState -@OptIn(ExperimentalToolbarApi::class) @Composable fun HomeRoute( padding: PaddingValues, modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), navigateToVideoDetail: (VideoType, Long, String?, Long) -> Unit, + navigateToPlaceDetail: (Long) -> Unit, navigateToUpload: () -> Unit = {}, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val toolbarScaffoldState = rememberCollapsingToolbarScaffoldState() - val coroutineScope = rememberCoroutineScope() + LaunchedEffectWithLifecycle { - viewModel.getVideos() viewModel.sideEffect.collectLatest { sideEffect -> when (sideEffect) { HomeSideEffect.navigateToUpload -> navigateToUpload() is HomeSideEffect.navigateToVideo -> { - navigateToVideoDetail(sideEffect.type, sideEffect.id, sideEffect.keyword, 0) + // navigateToVideoDetail(sideEffect.type, sideEffect.id, sideEffect.keyword, 0) } - HomeSideEffect.collapseToolbar -> { - coroutineScope.launch { - toolbarScaffoldState.toolbarState.collapse(500) - } + HomeSideEffect.launchSettingIntent -> TODO() + is HomeSideEffect.navigateToDetail -> { + navigateToPlaceDetail(sideEffect.id) } } } @@ -93,271 +80,181 @@ fun HomeRoute( HomeScreen( modifier = modifier.padding(bottom = padding.calculateBottomPadding()), state = state, - toolbarState = toolbarScaffoldState, - onUploadButtonClick = viewModel::navigateToUpload, - onChipButtonClick = viewModel::selectCategory, - onVideoClick = viewModel::navigateToVideo, - onBookmarkClick = viewModel::bookmark, + showLocationPermissionDialog = viewModel::showLocationPermissionDialog, + updateLocation = viewModel::updateLocation, + getData = viewModel::getPlaces, + navigateToDetail = viewModel::navigateToDetail, ) } @Composable fun HomeScreen( modifier: Modifier = Modifier, - toolbarState: CollapsingToolbarScaffoldState, state: HomeState, - onUploadButtonClick: () -> Unit, - onChipButtonClick: (Int) -> Unit, - onVideoClick: (Long, VideoType) -> Unit, - onBookmarkClick: (Long) -> Unit, + showLocationPermissionDialog: (Boolean) -> Unit, + updateLocation: (Double, Double) -> Unit, + getData: () -> Unit, + navigateToDetail: (Long) -> Unit, ) { - var boxSize by remember { - mutableStateOf(IntSize.Zero) - } - Box( - modifier = modifier - .fillMaxSize() - .onGloballyPositioned { layoutCoordinates -> - boxSize = layoutCoordinates.size + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val context = LocalContext.current + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (isGranted) { + val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + + // 위치 정보 요청 + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + return@rememberLauncherForActivityResult } - .background( - brush = Brush.verticalGradient( - listOf(Color(0x339babfb), Color(0x00000000)), - startY = boxSize.height.toFloat() * 0.0f, - endY = boxSize.height.toFloat() * 0.3f, - ), - ), - ) { - BackgroundAnimation() - CollapsingToolbar( - toolbarState = toolbarState, - state = state, - onChipButtonClick = onChipButtonClick, - onVideoClick = onVideoClick, - onBookmarkClick = onBookmarkClick, - ) - UploadFloatingButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(bottom = 15.dp, end = 16.dp), - onClick = onUploadButtonClick, - ) - if (state.isLoading) { - LoadingLottie() + fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? -> + location?.let { + updateLocation(it.latitude, it.longitude) + Log.e("위치", "${it.latitude} ${it.longitude}") + getData() + } + } + showLocationPermissionDialog(false) + } else { + showLocationPermissionDialog(true) } } -} - -@Composable -fun BoxScope.BackgroundAnimation() { - val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.bubble)) - val progress by animateLottieCompositionAsState( - composition, - iterations = LottieConstants.IterateForever, - speed = 1.0f, - ) - LottieAnimation( - composition, - { progress }, - modifier = Modifier - .align(Alignment.TopCenter), - ) -} - -@Composable -fun LoadingLottie() { - val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.loading_lotties)) - val progress by animateLottieCompositionAsState( - composition, - iterations = LottieConstants.IterateForever, - speed = 4.0f, - ) - Box( - modifier = Modifier - .fillMaxSize() - .customClickable(rippleEnabled = false) {} - .background(color = RecordyTheme.colors.black50), - ) { - LottieAnimation( - composition, - { progress }, - modifier = Modifier - .align(Alignment.Center), - ) + val lazyColumnState = rememberLazyListState() + lazyColumnState.OnBottomReached(buffer = 2) { + getData() } -} -@Composable -fun CollapsingToolbar( - toolbarState: CollapsingToolbarScaffoldState, - state: HomeState, - onChipButtonClick: (Int) -> Unit, - onVideoClick: (Long, VideoType) -> Unit, - onBookmarkClick: (Long) -> Unit, -) { - CollapsingToolbarScaffold( - modifier = Modifier - .fillMaxSize(), - state = toolbarState, - scrollStrategy = ScrollStrategy.ExitUntilCollapsed, - enabled = true, - toolbar = { - ToolbarContent(toolbarState) - }, - ) { - ChipRow(toolbarState, state.chipList, state.selectedChipIndex, onChipButtonClick) - Content( - state = state, - onVideoClick = onVideoClick, - onBookmarkClick = onBookmarkClick, + LaunchedEffectWithLifecycle { + launcher.launch( + Manifest.permission.ACCESS_FINE_LOCATION, ) } -} -@Composable -fun CollapsingToolbarScope.ToolbarContent(toolbarState: CollapsingToolbarScaffoldState) { - val topPadding = (32 + 12 * toolbarState.toolbarState.progress).dp - val alpha = toolbarState.toolbarState.progress * 2 - 0.5f - Image( - painter = painterResource(R.drawable.ic_recordy_logo), - contentDescription = "logo", - modifier = Modifier - .padding(start = 16.dp, top = topPadding, bottom = 12.dp) - .pin(), - ) Box( - modifier = Modifier - .fillMaxWidth() - .height(246.dp) - .road(Alignment.CenterEnd, Alignment.BottomStart) - .alpha(alpha) - .padding(start = 16.dp), + modifier = modifier + .fillMaxSize(), ) { - Row( + LazyColumn( + state = lazyColumnState, modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, + .fillMaxSize(), ) { - Text( - text = "오늘은 어떤 키워드로\n공간을 둘러볼까요?", - modifier = Modifier - .weight(192f) - .padding(bottom = 28.dp), - style = RecordyTheme.typography.title1, - color = RecordyTheme.colors.white, - ) - - Image( - modifier = Modifier - .weight(140f) - .padding(end = 12.dp), - painter = painterResource(R.drawable.img_home_graphic), - contentDescription = "home", - ) + item { + Box { + Icon( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 16.dp) + .padding(top = 66.dp, bottom = 32.dp), + painter = painterResource(id = R.drawable.ic_viskit_logo), + tint = RecordyTheme.colors.viskitYellow500, + contentDescription = "logo", + ) + } + } + itemsIndexed(state.exhibitionList) { i, exhibition -> + ExhibitionContatiner( + modifier = Modifier.clickable { + navigateToDetail(exhibition.placeId.toLong()) + }, + exhibition, + screenWidth, + ) + } } - } -} -@OptIn(ExperimentalToolbarApi::class) -@Composable -fun ChipRow( - state: CollapsingToolbarScaffoldState, - chipList: List, - selectedChip: Int?, - onChipButtonClick: (Int) -> Unit, -) { - LazyRow( - modifier = Modifier - .padding(bottom = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - item { Spacer(modifier = Modifier.width(8.dp)) } - itemsIndexed(chipList) { i, item -> - RecordyChipButton( - text = item, - isActive = selectedChip == i, - onClick = { - onChipButtonClick(i) - }, + if (state.showLocationPermissionDialog) { + RecordyDialog( + graphicAsset = R.drawable.img_trashcan, + title = "필수 권한 허용해 주세요", + subTitle = "내 위치 기반 공간 추천을 위해\n사용자의 위치에 접근하도록 허용해 주세요.", + negativeButtonLabel = "취소", + positiveButtonLabel = "삭제", + onDismissRequest = { }, + onPositiveButtonClick = { }, ) } - item { Spacer(modifier = Modifier.width(8.dp)) } } } @Composable -fun Content( - state: HomeState, - onVideoClick: (Long, VideoType) -> Unit, - onBookmarkClick: (Long) -> Unit, -) { - val configuration = LocalConfiguration.current - val screenWidth = configuration.screenWidthDp.dp - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(top = 44.dp), - ) { - item { - Section( - title = "이번 주 인기 기록", - videoList = state.popularList, - screenWidth = screenWidth, - onVideoClick = onVideoClick, - onBookmarkClick = onBookmarkClick, - videoType = VideoType.POPULAR, - ) - Section( - title = "방금 막 올라왔어요", - videoList = state.recentList, - screenWidth = screenWidth, - onVideoClick = onVideoClick, - onBookmarkClick = onBookmarkClick, - videoType = VideoType.RECENT, - ) - Spacer(modifier = Modifier.height(56.dp)) - } - } -} - -@Composable -fun Section( - title: String, - videoList: List, +private fun ExhibitionContatiner( + modifier: Modifier, + place: Place, screenWidth: Dp, - onVideoClick: (Long, VideoType) -> Unit, - onBookmarkClick: (Long) -> Unit, - videoType: VideoType, + onVideoClick: (Long, VideoType) -> Unit = { i, j -> }, + onBookmarkClick: (Long) -> Unit = {}, + videoType: VideoType = VideoType.RECENT, ) { Column( - modifier = Modifier - .fillMaxWidth(), + modifier = modifier, ) { - Text( - text = title, - style = RecordyTheme.typography.subtitle, - color = RecordyTheme.colors.white, + Row( modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 16.dp, bottom = 12.dp), - ) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp) + .background(color = RecordyTheme.colors.gray10, shape = RoundedCornerShape(8.dp)), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (place.address.isNotBlank()) { + Text( + text = place.address, + style = RecordyTheme.typography.caption1M, + color = RecordyTheme.colors.gray05, + ) + } + Text( + text = place.name, + style = RecordyTheme.typography.title3, + color = RecordyTheme.colors.gray01, + ) + Row { + Text( + text = place.exhibitionCount.toString() + "개", + style = RecordyTheme.typography.body2SB, + color = RecordyTheme.colors.viskitYellow500, + ) + Text( + text = "의 전시가 진행 중이에요.", + style = RecordyTheme.typography.body2SB, + color = RecordyTheme.colors.gray02, + ) + } + } + Icon( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(16.dp), + tint = RecordyTheme.colors.gray01, + painter = painterResource(id = R.drawable.ic_angle_right_24), + contentDescription = "next", + ) + } + LazyRow( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { item { Spacer(modifier = Modifier.width(4.dp)) } - itemsIndexed(videoList) { index, videoData -> - RecordyVideoThumbnail( - modifier = Modifier.width(screenWidth / 8 * 3), - imageUri = videoData.previewUrl, - location = videoData.location, - isBookmarkable = true, - isBookmark = videoData.isBookmark, - onClick = { onVideoClick(videoData.id, videoType) }, - onBookmarkClick = { onBookmarkClick(videoData.id) }, - ) + if (place.exhibitionRecord != null) { + itemsIndexed(place.exhibitionRecord!!) { index, videoData -> + RecordyVideoThumbnail( + modifier = Modifier.width(screenWidth / 8 * 3), + imageUri = videoData.previewUrl, + location = videoData.location, + isBookmarkable = true, + isBookmark = videoData.isBookmark, + onClick = { onVideoClick(videoData.id, videoType) }, + onBookmarkClick = { onBookmarkClick(videoData.id) }, + ) + } } + item { Spacer(modifier = Modifier.width(4.dp)) } } } @@ -367,5 +264,14 @@ fun Section( @Composable fun PreviewHome() { RecordyTheme { + HomeScreen( + state = HomeState( + exhibitionList = emptyList().toImmutableList(), + ), + showLocationPermissionDialog = {}, + updateLocation = { i, j -> }, + getData = {}, + navigateToDetail = {}, + ) } } diff --git a/feature/home/src/main/java/com/record/home/HomeViewModel.kt b/feature/home/src/main/java/com/record/home/HomeViewModel.kt index 965cb915..26b042f2 100644 --- a/feature/home/src/main/java/com/record/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/record/home/HomeViewModel.kt @@ -2,8 +2,8 @@ package com.record.home import android.util.Log import androidx.lifecycle.viewModelScope -import com.record.keyword.repository.KeywordRepository -import com.record.model.VideoType +import com.record.exhibition.model.Place +import com.record.exhibition.repository.ExhibitionRepository import com.record.model.exception.ApiError import com.record.ui.base.BaseViewModel import com.record.video.repository.VideoRepository @@ -15,139 +15,89 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val videoRepository: VideoRepository, - private val keywordRepository: KeywordRepository, + private val exhibitionRepository: ExhibitionRepository, ) : BaseViewModel(HomeState()) { - fun navigateToUpload() { - postSideEffect(HomeSideEffect.navigateToUpload) + fun navigateToVideo(videoId: Long, location: String) { + postSideEffect(HomeSideEffect.navigateToVideo(videoId, location)) } - fun selectCategory(categoryIndex: Int) { - intent { - copy(selectedChipIndex = categoryIndex) - } - postSideEffect(HomeSideEffect.collapseToolbar) - getPopularVideos() - getRecentVideos() + fun navigateToDetail(placeId: Long) { + postSideEffect(HomeSideEffect.navigateToDetail(placeId)) } - fun getVideos() { - getPreferenceKeywords() - getPopularVideos() - getRecentVideos() - } - - private fun getPreferenceKeywords() { - viewModelScope.launch { - val newList = listOf("전체") - keywordRepository.getKeywords().onSuccess { + fun getPlaces() = viewModelScope.launch { + if (uiState.value.isEnd) return@launch + val list = uiState.value.exhibitionList + exhibitionRepository.getNearPlaceData(uiState.value.page, 10, uiState.value.location.latitude, uiState.value.location.longitude) + .onSuccess { intent { - copy(chipList = (newList + it.keywords).toImmutableList()) + copy(exhibitionList = (list + it.data).toImmutableList()) } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("error", it.message) - } - } - } - } - } - - private fun getRecentVideos() { - viewModelScope.launch { - val keyIndex = uiState.value.selectedChipIndex - val keyword = if (keyIndex != null) listOf(uiState.value.chipList[keyIndex]) else null - videoRepository.getRecentVideos( - keywords = keyword, - cursor = 0, - pageSize = 10, - ).onSuccess { intent { - copy(recentList = it.data.toImmutableList()) + copy(isEnd = !it.hasNext, page = uiState.value.page + 1) } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("error", it.message) - } + } + .onFailure { throwable -> + if (throwable is ApiError) { + Log.e("asdfasdf", throwable.message) + } else { + Log.e("asdfasdf", throwable.message.toString()) } } - } } - private fun getPopularVideos() { - viewModelScope.launch { - val keyIndex = uiState.value.selectedChipIndex - val keyword = if (keyIndex != null) listOf(uiState.value.chipList[keyIndex]) else null - videoRepository.getPopularVideos( - keywords = keyword, - pageNumber = 0, - pageSize = 10, - ).onSuccess { - intent { - copy(popularList = it.data.toImmutableList()) - } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("error", it.message) - } - } - } - } + fun showLocationPermissionDialog(isShow: Boolean) = intent { + copy(showLocationPermissionDialog = isShow) } - fun navigateToVideo(videoId: Long, type: VideoType) { - val selectedIndex = uiState.value.selectedChipIndex - val selectedKeyword = if (selectedIndex != null) uiState.value.chipList[selectedIndex] else null - postSideEffect(HomeSideEffect.navigateToVideo(videoId, type, selectedKeyword)) + fun updateLocation(latitude: Double, longitude: Double) = intent { + copy(location = Location(latitude, longitude)) } fun bookmark(id: Long) { intent { - val updatedRecentList = uiState.value.recentList.map { video -> - if (video.id == id) { - video.copy(isBookmark = !video.isBookmark) - } else { - video - } - } - - val updatedPopularList = uiState.value.popularList.map { video -> - if (video.id == id) { - video.copy(isBookmark = !video.isBookmark) - } else { - video - } + val updatedList = uiState.value.exhibitionList.map { exhibition -> + Place( + placeId = exhibition.placeId, + address = exhibition.address, + name = exhibition.name, + exhibitionCount = exhibition.exhibitionCount, + recordCount = exhibition.recordCount, + exhibitionRecord = exhibition.exhibitionRecord?.map { video -> + if (video.id == id) { + video.copy(isBookmark = !video.isBookmark) + } else { + video + } + }?.toImmutableList(), + ) } copy( - recentList = updatedRecentList.toImmutableList(), - popularList = updatedPopularList.toImmutableList(), + exhibitionList = updatedList.toImmutableList(), ) } viewModelScope.launch { videoRepository.bookmark(id).onSuccess { - val updatedRecentList1 = uiState.value.recentList.map { video -> - if (video.id == id) { - video.copy(isBookmark = it) - } else { - video - } - } - - val updatedPopularList1 = uiState.value.popularList.map { video -> - if (video.id == id) { - video.copy(isBookmark = it) - } else { - video - } + val updatedList = uiState.value.exhibitionList.map { exhibition -> + Place( + placeId = exhibition.placeId, + address = exhibition.address, + name = exhibition.name, + exhibitionCount = exhibition.exhibitionCount, + recordCount = exhibition.recordCount, + exhibitionRecord = exhibition.exhibitionRecord?.map { video -> + if (video.id == id) { + video.copy(isBookmark = !video.isBookmark) + } else { + video + } + }?.toImmutableList(), + ) } - intent { copy( - recentList = updatedRecentList1.toImmutableList(), - popularList = updatedPopularList1.toImmutableList(), + exhibitionList = updatedList.toImmutableList(), ) } }.onFailure { diff --git a/feature/home/src/main/java/com/record/home/Location.kt b/feature/home/src/main/java/com/record/home/Location.kt new file mode 100644 index 00000000..02e15874 --- /dev/null +++ b/feature/home/src/main/java/com/record/home/Location.kt @@ -0,0 +1,6 @@ +package com.record.home + +data class Location( + val latitude: Double, + val longitude: Double, +) diff --git a/feature/home/src/main/java/com/record/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/record/home/navigation/HomeNavigation.kt index 7be0353e..778b0d64 100644 --- a/feature/home/src/main/java/com/record/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/record/home/navigation/HomeNavigation.kt @@ -18,6 +18,7 @@ fun NavGraphBuilder.homeNavGraph( modifier: Modifier = Modifier, navigateToVideoDetail: (VideoType, Long, String?, Long) -> Unit, navigateToUpload: () -> Unit = {}, + navigateToPlaceDetail: (Long) -> Unit = {}, ) { composable(route = HomeRoute.route) { HomeRoute( @@ -25,6 +26,7 @@ fun NavGraphBuilder.homeNavGraph( modifier = modifier, navigateToVideoDetail = navigateToVideoDetail, navigateToUpload = navigateToUpload, + navigateToPlaceDetail = navigateToPlaceDetail, ) } } diff --git a/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt b/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt index eb87468b..35408be6 100644 --- a/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt +++ b/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt @@ -256,7 +256,7 @@ fun CustomTabRow( val animatedIndicatorWidth by animateDpAsState( targetValue = tabWidth - 12.dp, - animationSpec = tween(200), + animationSpec = tween(0), ) val density = LocalDensity.current diff --git a/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt index 410dc9e5..ede29fa1 100644 --- a/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt @@ -128,8 +128,8 @@ internal class MainNavigator( navController.navigateSetting(navOptions { }) } - fun navigateDetail() { - navController.navigateDetail(navOptions { }) + fun navigateDetail(id: Long) { + navController.navigateDetail(placeId = id, navOptions { }) } fun navigateSearch() { diff --git a/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt index 3d0a0728..c8fadf6e 100644 --- a/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt @@ -94,6 +94,7 @@ internal fun MainScreen( padding = innerPadding, navigateToVideoDetail = navigator::navigateVideoDetail, navigateToUpload = navigator::navigateToUpload, + navigateToPlaceDetail = navigator::navigateDetail, ) profileNavGraph( diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index c5b7cce5..5cc378ef 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -5,3 +5,7 @@ plugins { android { namespace = "com.record.search" } + +dependencies { + implementation(projects.domain.exhibition) +} diff --git a/feature/search/src/main/java/com/record/search/SearchScreen.kt b/feature/search/src/main/java/com/record/search/SearchScreen.kt index eae541cf..0662700b 100644 --- a/feature/search/src/main/java/com/record/search/SearchScreen.kt +++ b/feature/search/src/main/java/com/record/search/SearchScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.record.designsystem.R import com.record.designsystem.theme.RecordyTheme +import com.record.exhibition.model.SearchResult import com.record.search.component.SearchBox import com.record.search.component.SearchedContainerBtn import com.record.search.component.SearchingContainerBtn @@ -56,7 +57,7 @@ fun SearchScreen( modifier: Modifier, query: String, onQueryChange: (String) -> Unit, - items: List, + items: List, ) { val keyboardController = LocalSoftwareKeyboardController.current @@ -92,10 +93,10 @@ fun SearchScreen( Column { SearchedContainerBtn( modifier = modifier.fillMaxWidth(), - exhibitionName = item.exhibitionName, - location = item.location, - venue = item.venue, - type = item.listOf, + exhibitionName = item.name, + location = item.address, + venue = item.name, + type = item.type, ) HorizontalDivider( modifier = modifier @@ -118,9 +119,9 @@ fun SearchScreen( items(items) { item -> SearchingContainerBtn( modifier = modifier.fillMaxWidth(), - exhibitionName = item.exhibitionName, - location = item.location, - venue = item.venue, + exhibitionName = item.name, + location = item.address, + venue = item.name, ) } } @@ -191,6 +192,7 @@ fun DefaultSearchUI() { modifier = Modifier.weight(1f), ) { Text( + modifier = Modifier.padding(bottom = 2.dp), text = "공간뿐만 아니라 원하는 전시회를 찾고 싶다면?", style = RecordyTheme.typography.caption1M, color = RecordyTheme.colors.gray05, @@ -199,7 +201,7 @@ fun DefaultSearchUI() { Row( modifier = Modifier .fillMaxWidth() - .padding(bottom = 2.dp), + .padding(top = 4.dp), ) { Text( text = "\'전시회명\'", diff --git a/feature/search/src/main/java/com/record/search/SearchState.kt b/feature/search/src/main/java/com/record/search/SearchState.kt index 56df46a4..7e08fc0d 100644 --- a/feature/search/src/main/java/com/record/search/SearchState.kt +++ b/feature/search/src/main/java/com/record/search/SearchState.kt @@ -1,5 +1,6 @@ package com.record.search +import com.record.exhibition.model.SearchResult import com.record.ui.base.SideEffect import com.record.ui.base.UiState import kotlinx.collections.immutable.ImmutableList @@ -7,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList data class SearchState( val query: String = "", - val filteredItems: ImmutableList = emptyList().toImmutableList(), + val filteredItems: ImmutableList = emptyList().toImmutableList(), ) : UiState sealed interface SearchSideEffect : SideEffect diff --git a/feature/search/src/main/java/com/record/search/SearchViewModel.kt b/feature/search/src/main/java/com/record/search/SearchViewModel.kt index ac661dfc..578ee178 100644 --- a/feature/search/src/main/java/com/record/search/SearchViewModel.kt +++ b/feature/search/src/main/java/com/record/search/SearchViewModel.kt @@ -1,41 +1,36 @@ package com.record.search import androidx.lifecycle.viewModelScope +import com.record.exhibition.repository.SearchRepository import com.record.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch +import javax.inject.Inject -class SearchViewModel : BaseViewModel( +@HiltViewModel +class SearchViewModel @Inject constructor( + private val searchRepository: SearchRepository, +) : BaseViewModel( initialState = SearchState(), ) { - private val items = listOf( - ExhibitionData("국립현대미술관", "서울 종로구", "전시회장", listOf("미술전시회1", "전시회2", "전시회3")), - ExhibitionData("국으로 시작하는 단어", "서울 종로구", "전시회장", listOf("미술", "현대미술")), - ExhibitionData("서울 예술의 전당", "서울 서초구", "전시회장", listOf("음악")), - ExhibitionData("D Museum", "서울 용산구", "미술관", listOf("사진", "디자인")), - ) + init { + viewModelScope.launch { + uiState.debounce(200).collectLatest { + searchRepository.searchExhibition(it.query).onSuccess { + intent { + copy(filteredItems = it.toImmutableList()) + } + } + } + } + } fun onQueryChanged(newQuery: String) { intent { copy(query = newQuery) } - filterItems(newQuery) - } - - private fun filterItems(query: String) { - viewModelScope.launch { - val result = if (query.isEmpty()) { - items - } else { - items.filter { - it.exhibitionName.contains(query, ignoreCase = true) || - it.location.contains(query, ignoreCase = true) || - it.venue.contains(query, ignoreCase = true) - } - } - intent { - copy(filteredItems = result.toImmutableList()) - } - } } } diff --git a/feature/search/src/main/java/com/record/search/component/SearchBox.kt b/feature/search/src/main/java/com/record/search/component/SearchBox.kt index 5b8384ee..832c0b4b 100644 --- a/feature/search/src/main/java/com/record/search/component/SearchBox.kt +++ b/feature/search/src/main/java/com/record/search/component/SearchBox.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -84,6 +85,9 @@ fun SearchBox( } innerTextField() }, + cursorBrush = SolidColor(RecordyTheme.colors.gray02), + maxLines = 1, + singleLine = true, ) } } diff --git a/feature/search/src/main/java/com/record/search/component/SearchedContainerBtn.kt b/feature/search/src/main/java/com/record/search/component/SearchedContainerBtn.kt index 290336bd..ea99382f 100644 --- a/feature/search/src/main/java/com/record/search/component/SearchedContainerBtn.kt +++ b/feature/search/src/main/java/com/record/search/component/SearchedContainerBtn.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.record.designsystem.R import com.record.designsystem.theme.RecordyTheme +import com.record.exhibition.model.ResultType @Composable fun SearchedContainerBtn( @@ -27,7 +28,7 @@ fun SearchedContainerBtn( exhibitionName: String, location: String, venue: String, - type: List, + type: ResultType, ) { Box( modifier = modifier @@ -77,10 +78,10 @@ fun SearchedContainerBtn( Column( modifier = modifier.padding(horizontal = 8.dp), ) { - type.take(3).forEachIndexed { index, title -> + /*type.take(3).forEachIndexed { index, title -> if (index > 0) Spacer(modifier = modifier.height(8.dp)) ExhibitionTitle(type = title) - } + }*/ } Spacer(modifier = modifier.height(24.dp)) @@ -96,7 +97,7 @@ fun SearchedContainerBtnPreview() { exhibitionName = "전시회명", location = "위치", venue = "장소", - type = listOf("전시회", "공간", "장소"), + type = ResultType.PLACE, ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 71518883..1b3c8753 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,6 +61,7 @@ google-service = "4.4.2" material = "1.12.0" firebase-bom = "33.1.1" crashlytics = "3.0.2" +google-location = "21.3.0" # Compose Versions compose-compiler = "1.5.1" @@ -219,6 +220,7 @@ firebase-database = { group = "com.google.firebase", name = "firebase-database-k accompanist-insets = { module = "com.google.accompanist:accompanist-insets", version.ref = "accompanistInsets" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistPermissions" } +google-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "google-location"} # AWS aws-android-sdk-cognito = { module = "com.amazonaws:aws-android-sdk-cognito", version.ref = "awsAndroidSdkMobileClient" } diff --git a/remote/exhibition/.gitignore b/remote/exhibition/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/remote/exhibition/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/remote/exhibition/build.gradle.kts b/remote/exhibition/build.gradle.kts new file mode 100644 index 00000000..897070d3 --- /dev/null +++ b/remote/exhibition/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.recordy.remote) +} + +android { + namespace = "com.record.exhibition" +} + +dependencies { + implementation(projects.data.exhibition) +} diff --git a/remote/exhibition/src/main/AndroidManifest.xml b/remote/exhibition/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/remote/exhibition/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/remote/exhibition/src/main/java/com/record/exhibition/api/ExhibitionApi.kt b/remote/exhibition/src/main/java/com/record/exhibition/api/ExhibitionApi.kt new file mode 100644 index 00000000..784275c5 --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/api/ExhibitionApi.kt @@ -0,0 +1,44 @@ +package com.record.exhibition.api + +import com.example.exhibition.model.remote.request.RequestPatchExhibitionDto +import com.example.exhibition.model.remote.request.RequestPostExhibitionDto +import com.example.exhibition.model.remote.response.ResponseGetExhibitionsDto +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface ExhibitionApi { + @GET("/api/v1/exhibitions") + suspend fun getExhibitionByPlaceId( + @Query("placeId") placeId: Int, + ): List + + @POST("/api/v1/exhibitions") + suspend fun postExhibition( + @Body requestPostExhibitionDto: RequestPostExhibitionDto, + ) + + @PATCH("/api/v1/exhibitions") + suspend fun patchExhibition( + @Body requestPatchExhibitionDto: RequestPatchExhibitionDto, + ) + + @GET("/api/v1/exhibitions/free") + suspend fun getFreeExhibitions( + @Query("placeId") placeId: Int, + ): List + + @GET("/api/v1/exhibitions/closing") + suspend fun getClosingExhibitions( + @Query("placeId") placeId: Int, + ): List + + @DELETE("/api/v1/exhibitions/{exhibitionId}") + suspend fun deleteExhibitionById( + @Path("exhibitionId") exhibitionId: Int, + ) +} diff --git a/remote/exhibition/src/main/java/com/record/exhibition/api/PlaceApi.kt b/remote/exhibition/src/main/java/com/record/exhibition/api/PlaceApi.kt new file mode 100644 index 00000000..66b6eeb4 --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/api/PlaceApi.kt @@ -0,0 +1,43 @@ +package com.record.exhibition.api + +import com.example.exhibition.model.remote.request.RequestPostPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetPagingPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetReviewsDto +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface PlaceApi { + @POST("/api/v1/places") + suspend fun postPlace( + @Body requestPostPlaceDto: RequestPostPlaceDto, + ) + + @GET("/api/v1/places/{id}") + suspend fun getPlaceById( + @Path("id") id: Int, + ): ResponseGetPlaceDto + + @GET("/api/v1/places/{id}/reviews") + suspend fun getReviewsById( + @Path("id") id: Int, + ): List + + @GET("/api/v1/places/exhibitions/geography") + suspend fun getNearPlaces( + @Query("number") number: Int, + @Query("size") size: Int, + @Query("latitude") latitude: Double, + @Query("longitude") longitude: Double, + @Query("distance") distance: Double, + ): ResponseGetPagingPlaceDto + + @GET("/api/v1/places/exhibitions/date") + suspend fun getHasInProgressExhibitionPlaces( + @Query("number") number: Int, + @Query("size") size: Int, + ): List +} diff --git a/remote/exhibition/src/main/java/com/record/exhibition/api/SearchApi.kt b/remote/exhibition/src/main/java/com/record/exhibition/api/SearchApi.kt new file mode 100644 index 00000000..f9d753a8 --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/api/SearchApi.kt @@ -0,0 +1,12 @@ +package com.record.exhibition.api + +import com.example.exhibition.model.remote.response.ResponseGetExhibitionSearchDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface SearchApi { + @GET("/api/v1/search") + suspend fun getExhibitionSearch( + @Query("query") query: String, + ): List +} diff --git a/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemoteExhibitionDataSourceImpl.kt b/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemoteExhibitionDataSourceImpl.kt new file mode 100644 index 00000000..dc7abd9e --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemoteExhibitionDataSourceImpl.kt @@ -0,0 +1,35 @@ +package com.record.exhibition.datasource + +import com.example.exhibition.model.remote.request.RequestPatchExhibitionDto +import com.example.exhibition.model.remote.request.RequestPostExhibitionDto +import com.example.exhibition.source.remote.RemoteExhibitionDataSource +import com.record.exhibition.api.ExhibitionApi +import javax.inject.Inject + +class RemoteExhibitionDataSourceImpl @Inject constructor( + private val exhibitionApi: ExhibitionApi, +) : RemoteExhibitionDataSource { + override suspend fun postExhibition( + requestPostExhibitionDto: RequestPostExhibitionDto, + ) = exhibitionApi.postExhibition(requestPostExhibitionDto) + + override suspend fun getExhibitionById( + placeId: Int, + ) = exhibitionApi.getExhibitionByPlaceId(placeId) + + override suspend fun getFreeExhibition( + placeId: Int, + ) = exhibitionApi.getFreeExhibitions(placeId) + + override suspend fun getClosingExhibition( + placeId: Int, + ) = exhibitionApi.getClosingExhibitions(placeId) + + override suspend fun patchExhibition( + requestPatchExhibitionDto: RequestPatchExhibitionDto, + ) = exhibitionApi.patchExhibition(requestPatchExhibitionDto) + + override suspend fun deleteExhibition( + exhibitionId: Int, + ) = exhibitionApi.deleteExhibitionById(exhibitionId) +} diff --git a/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemotePlaceDataSourceImpl.kt b/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemotePlaceDataSourceImpl.kt new file mode 100644 index 00000000..d6bebab9 --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemotePlaceDataSourceImpl.kt @@ -0,0 +1,28 @@ +package com.record.exhibition.datasource + +import com.example.exhibition.model.remote.request.RequestPostPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetPagingPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetPlaceDto +import com.example.exhibition.model.remote.response.ResponseGetReviewsDto +import com.example.exhibition.source.remote.RemotePlaceDataSource +import com.record.exhibition.api.PlaceApi +import javax.inject.Inject + +class RemotePlaceDataSourceImpl @Inject constructor( + private val placeApi: PlaceApi, +) : RemotePlaceDataSource { + override suspend fun postPlace(requestPostPlaceDto: RequestPostPlaceDto) = + placeApi.postPlace(requestPostPlaceDto) + + override suspend fun getPlaceById(id: Int): ResponseGetPlaceDto = + placeApi.getPlaceById(id) + + override suspend fun getReviewsById(id: Int): List = + placeApi.getReviewsById(id) + + override suspend fun getNearPlace(number: Int, size: Int, latitude: Double, longitude: Double, distance: Double): ResponseGetPagingPlaceDto = + placeApi.getNearPlaces(number, size, latitude, longitude, distance) + + override suspend fun getHasInProgressExhibitionPlaces(number: Int, size: Int): List = + placeApi.getHasInProgressExhibitionPlaces(number, size) +} diff --git a/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemoteSearchDataSourceImpl.kt b/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemoteSearchDataSourceImpl.kt new file mode 100644 index 00000000..4bc0f6fe --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/datasource/RemoteSearchDataSourceImpl.kt @@ -0,0 +1,12 @@ +package com.record.exhibition.datasource + +import com.example.exhibition.model.remote.response.ResponseGetExhibitionSearchDto +import com.example.exhibition.source.remote.RemoteSearchDataSource +import com.record.exhibition.api.SearchApi +import javax.inject.Inject + +class RemoteSearchDataSourceImpl @Inject constructor( + private val searchApi: SearchApi, +) : RemoteSearchDataSource { + override suspend fun searchExhibition(query: String): List = searchApi.getExhibitionSearch(query) +} diff --git a/remote/exhibition/src/main/java/com/record/exhibition/di/ApiModule.kt b/remote/exhibition/src/main/java/com/record/exhibition/di/ApiModule.kt new file mode 100644 index 00000000..fab8b8c8 --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/di/ApiModule.kt @@ -0,0 +1,29 @@ +package com.record.exhibition.di + +import com.record.exhibition.api.ExhibitionApi +import com.record.exhibition.api.PlaceApi +import com.record.exhibition.api.SearchApi +import com.record.network.di.Auth +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.create +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object ApiModule { + @Provides + @Singleton + fun providesExhibitionApi(@Auth retrofit: Retrofit): ExhibitionApi = retrofit.create() + + @Provides + @Singleton + fun providesPlaceApi(@Auth retrofit: Retrofit): PlaceApi = retrofit.create() + + @Provides + @Singleton + fun providesSearchApi(@Auth retrofit: Retrofit): SearchApi = retrofit.create() +} diff --git a/remote/exhibition/src/main/java/com/record/exhibition/di/DataModule.kt b/remote/exhibition/src/main/java/com/record/exhibition/di/DataModule.kt new file mode 100644 index 00000000..d342f515 --- /dev/null +++ b/remote/exhibition/src/main/java/com/record/exhibition/di/DataModule.kt @@ -0,0 +1,29 @@ +package com.record.exhibition.di + +import com.example.exhibition.source.remote.RemoteExhibitionDataSource +import com.example.exhibition.source.remote.RemotePlaceDataSource +import com.example.exhibition.source.remote.RemoteSearchDataSource +import com.record.exhibition.datasource.RemoteExhibitionDataSourceImpl +import com.record.exhibition.datasource.RemotePlaceDataSourceImpl +import com.record.exhibition.datasource.RemoteSearchDataSourceImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +abstract class DataModule { + @Binds + @Singleton + abstract fun bindsRemoteExhibitionDataSource(remoteExhibitionDataSourceImpl: RemoteExhibitionDataSourceImpl): RemoteExhibitionDataSource + + @Binds + @Singleton + abstract fun bindsRemotePlaceDataSource(remotePlaceDataSourceImpl: RemotePlaceDataSourceImpl): RemotePlaceDataSource + + @Binds + @Singleton + abstract fun bindsRemoteSearchDataSource(remoteSearchDataSourceImpl: RemoteSearchDataSourceImpl): RemoteSearchDataSource +} diff --git a/remote/video/src/main/java/com/record/video/api/VideoApi.kt b/remote/video/src/main/java/com/record/video/api/VideoApi.kt index 9880957b..0f644433 100644 --- a/remote/video/src/main/java/com/record/video/api/VideoApi.kt +++ b/remote/video/src/main/java/com/record/video/api/VideoApi.kt @@ -30,6 +30,13 @@ interface VideoApi { @Query("pageSize") pageSize: Int, ): ResponseGetPagingVideoDto + @GET("/api/v1/records/place") + suspend fun getPlaceVideos( + @Query("placeId") placeId: Int, + @Query("cursorId") cursor: Long, + @Query("size") pageSize: Int, + ): ResponseGetSliceVideoDto + @GET("/api/v1/records/user/{otherUserId}") suspend fun getUserVideos( @Path("otherUserId") otherUserId: Long, diff --git a/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt b/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt index 84d2bbbf..23a23a8c 100644 --- a/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt +++ b/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt @@ -23,6 +23,9 @@ class RemoteVideoDataSourceImpl @Inject constructor( override suspend fun getPopularVideos(keywords: List?, pageNumber: Int, pageSize: Int): ResponseGetPagingVideoDto = videoApi.getPopularVideos(keywords, pageNumber, pageSize) + override suspend fun getPlaceVideos(placeId: Int, cursor: Long, pageSize: Int): ResponseGetSliceVideoDto = + videoApi.getPlaceVideos(placeId, cursor, pageSize) + override suspend fun getUserVideos(otherUserId: Long, cursorId: Long, size: Int): ResponseGetSliceVideoDto = videoApi.getUserVideos(otherUserId, cursorId, size) diff --git a/settings.gradle.kts b/settings.gradle.kts index 1a21edf0..ceb5f068 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,3 +64,6 @@ include(":core:security") include(":local:video") include(":feature:search") include(":feature:detail") +include(":domain:exhibition") +include(":remote:exhibition") +include(":data:exhibition")