diff --git a/app/src/main/java/com/depromeet/spot/di/DataSourceModule.kt b/app/src/main/java/com/depromeet/spot/di/DataSourceModule.kt index 50c884d0..cbd3c058 100644 --- a/app/src/main/java/com/depromeet/spot/di/DataSourceModule.kt +++ b/app/src/main/java/com/depromeet/spot/di/DataSourceModule.kt @@ -1,11 +1,13 @@ package com.depromeet.spot.di import com.depromeet.data.datasource.ExampleDataSource +import com.depromeet.data.datasource.HomeDataSource import com.depromeet.data.datasource.SeatReviewDataSource import com.depromeet.data.datasource.SignupRemoteDataSource import com.depromeet.data.datasource.ViewfinderDataSource import com.depromeet.data.datasource.WebSvgDataSource import com.depromeet.data.datasource.remote.ExampleDataSourcelmpl +import com.depromeet.data.datasource.remote.HomeDataSourceImpl import com.depromeet.data.datasource.remote.SeatReviewDataSourceImpl import com.depromeet.data.datasource.remote.SignupRemoteDataSourceImpl import com.depromeet.data.datasource.remote.WebSvgDataSourceImpl @@ -32,6 +34,12 @@ abstract class DataSourceModule { webSvgDataSourceImpl: WebSvgDataSourceImpl, ): WebSvgDataSource + @Binds + @Singleton + abstract fun bindHomeDataSource( + homeDataSourceImpl: HomeDataSourceImpl, + ): HomeDataSource + @Binds @Singleton abstract fun seatReviewDataSource( diff --git a/app/src/main/java/com/depromeet/spot/di/RepositoryModule.kt b/app/src/main/java/com/depromeet/spot/di/RepositoryModule.kt index dbaa23f1..c8eaa4b6 100644 --- a/app/src/main/java/com/depromeet/spot/di/RepositoryModule.kt +++ b/app/src/main/java/com/depromeet/spot/di/RepositoryModule.kt @@ -1,11 +1,13 @@ package com.depromeet.spot.di import com.depromeet.data.repository.ExampleRepositoryImpl +import com.depromeet.data.repository.HomeRepositoryImpl import com.depromeet.data.repository.SeatReviewRepositoryImpl import com.depromeet.data.repository.SignupRepositoryImpl import com.depromeet.data.repository.ViewfinderRepositoryImpl import com.depromeet.data.repository.WebSvgRepositoryImpl import com.depromeet.domain.repository.ExampleRepository +import com.depromeet.domain.repository.HomeRepository import com.depromeet.domain.repository.SeatReviewRepository import com.depromeet.domain.repository.SignupRepository import com.depromeet.domain.repository.ViewfinderRepository @@ -32,6 +34,12 @@ abstract class RepositoryModule { webSvgRepositoryImpl: WebSvgRepositoryImpl, ): WebSvgRepository + @Binds + @Singleton + abstract fun bindHomeRepository( + homeRepositoryImpl: HomeRepositoryImpl, + ): HomeRepository + @Binds @Singleton abstract fun seatReviewRepository( diff --git a/app/src/main/java/com/depromeet/spot/di/ServiceModule.kt b/app/src/main/java/com/depromeet/spot/di/ServiceModule.kt index f3d7a79c..776634d1 100644 --- a/app/src/main/java/com/depromeet/spot/di/ServiceModule.kt +++ b/app/src/main/java/com/depromeet/spot/di/ServiceModule.kt @@ -1,6 +1,7 @@ package com.depromeet.spot.di import com.depromeet.data.remote.ExampleService +import com.depromeet.data.remote.HomeApiService import com.depromeet.data.remote.SeatReviewService import com.depromeet.data.remote.SignupService import com.depromeet.data.remote.ViewfinderService @@ -31,6 +32,11 @@ object ServiceModule { fun provideWebSvgService(@WebSvg retrofit: Retrofit): WebSvgApiService = retrofit.create(WebSvgApiService::class.java) + @Provides + @Singleton + fun provideHomeService(retrofit: Retrofit): HomeApiService = + retrofit.create(HomeApiService::class.java) + @Provides @Singleton fun provideSeatReviewService(retrofit: Retrofit): SeatReviewService = diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 048e2cf5..559e0061 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -14,6 +16,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") + buildConfigField("String", "S3_URL", getApiKey("s3.base.url")) } buildTypes { @@ -37,6 +40,10 @@ android { } } +fun getApiKey(propertyKey: String): String { + return gradleLocalProperties(rootDir).getProperty(propertyKey) +} + dependencies { implementation(project(":domain")) KotlinDependencies.run { diff --git a/data/src/main/java/com/depromeet/data/datasource/HomeDataSource.kt b/data/src/main/java/com/depromeet/data/datasource/HomeDataSource.kt new file mode 100644 index 00000000..81707a21 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/datasource/HomeDataSource.kt @@ -0,0 +1,47 @@ +package com.depromeet.data.datasource + +import com.depromeet.data.model.request.home.RequestMySeatRecordDto +import com.depromeet.data.model.request.home.RequestProfileEditDto +import com.depromeet.data.model.response.home.ResponseBaseballTeamDto +import com.depromeet.data.model.response.home.ResponseDeleteReviewDto +import com.depromeet.data.model.response.home.ResponseMySeatRecordDto +import com.depromeet.data.model.response.home.ResponsePresignedUrlDto +import com.depromeet.data.model.response.home.ResponseProfileDto +import com.depromeet.data.model.response.home.ResponseProfileEditDto +import com.depromeet.data.model.response.home.ResponseRecentReviewDto +import com.depromeet.data.model.response.home.ResponseReviewDateDto + +interface HomeDataSource { + suspend fun getMySeatRecordData( + requestMySeatRecordDto: RequestMySeatRecordDto, + ): ResponseMySeatRecordDto + + suspend fun getBaseballTeamData(): List + + suspend fun postProfileImagePresigned( + fileExtension: String, + ): ResponsePresignedUrlDto + + suspend fun putProfileImage( + presignedUrl: String, + image: ByteArray, + ) + + suspend fun putProfileEdit( + requestProfileEditDto: RequestProfileEditDto, + ): ResponseProfileEditDto + + suspend fun getDuplicateNickname( + nickname: String, + ) + + suspend fun getReviewDate(): ResponseReviewDateDto + + suspend fun getProfile(): ResponseProfileDto + + suspend fun getRecentReview(): ResponseRecentReviewDto + + suspend fun deleteReview( + reviewId : Int + ): ResponseDeleteReviewDto +} \ No newline at end of file diff --git a/data/src/main/java/com/depromeet/data/datasource/remote/HomeDataSourceImpl.kt b/data/src/main/java/com/depromeet/data/datasource/remote/HomeDataSourceImpl.kt new file mode 100644 index 00000000..06b04e11 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/datasource/remote/HomeDataSourceImpl.kt @@ -0,0 +1,77 @@ +package com.depromeet.data.datasource.remote + +import com.depromeet.data.datasource.HomeDataSource +import com.depromeet.data.model.request.home.RequestFileExtensionDto +import com.depromeet.data.model.request.home.RequestMySeatRecordDto +import com.depromeet.data.model.request.home.RequestProfileEditDto +import com.depromeet.data.model.response.home.ResponseBaseballTeamDto +import com.depromeet.data.model.response.home.ResponseDeleteReviewDto +import com.depromeet.data.model.response.home.ResponseMySeatRecordDto +import com.depromeet.data.model.response.home.ResponsePresignedUrlDto +import com.depromeet.data.model.response.home.ResponseProfileDto +import com.depromeet.data.model.response.home.ResponseProfileEditDto +import com.depromeet.data.model.response.home.ResponseRecentReviewDto +import com.depromeet.data.model.response.home.ResponseReviewDateDto +import com.depromeet.data.remote.HomeApiService +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import javax.inject.Inject + +class HomeDataSourceImpl @Inject constructor( + private val homeApiService: HomeApiService, +) : HomeDataSource { + override suspend fun getMySeatRecordData(requestMySeatRecordDto: RequestMySeatRecordDto): ResponseMySeatRecordDto { + return homeApiService.getMySeatRecord( + page = requestMySeatRecordDto.page, + size = requestMySeatRecordDto.size, + year = requestMySeatRecordDto.year, + month = requestMySeatRecordDto.month + ) + } + + + override suspend fun getBaseballTeamData(): List { + return homeApiService.getBaseballTeam() + } + + override suspend fun postProfileImagePresigned( + fileExtension: String, + ): ResponsePresignedUrlDto { + return homeApiService.postProfileImagePresigned( + RequestFileExtensionDto(fileExtension), + ) + } + + override suspend fun putProfileImage(presignedUrl: String, image: ByteArray) { + val mediaType = "image/*".toMediaTypeOrNull() + return homeApiService.putProfileImage(presignedUrl, image.toRequestBody(mediaType)) + } + + override suspend fun putProfileEdit( + requestProfileEditDto: RequestProfileEditDto, + ): ResponseProfileEditDto { + return homeApiService.putProfileEdit(requestProfileEditDto) + } + + override suspend fun getDuplicateNickname(nickname: String) { + return homeApiService.getDuplicateNickname(nickname) + } + + override suspend fun getReviewDate(): ResponseReviewDateDto { + return homeApiService.getReviewDate() + } + + override suspend fun getProfile(): ResponseProfileDto { + return homeApiService.getProfileInfo() + } + + override suspend fun getRecentReview(): ResponseRecentReviewDto { + return homeApiService.getRecentReview() + } + + override suspend fun deleteReview( + reviewId: Int, + ): ResponseDeleteReviewDto { + return homeApiService.deleteReview(reviewId) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/depromeet/data/intercepter/AuthInterceptor.kt b/data/src/main/java/com/depromeet/data/intercepter/AuthInterceptor.kt index 85c32d87..e838eb02 100644 --- a/data/src/main/java/com/depromeet/data/intercepter/AuthInterceptor.kt +++ b/data/src/main/java/com/depromeet/data/intercepter/AuthInterceptor.kt @@ -1,8 +1,8 @@ package com.depromeet.data.intercepter +import com.depromeet.data.BuildConfig.S3_URL import com.depromeet.domain.preference.SharedPreference import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response @@ -17,7 +17,10 @@ class AuthInterceptor @Inject constructor( override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() - if (!shouldRequestAuthenticatedHeaders(originalRequest.url.encodedPath)) { + if (!shouldRequestAuthenticatedHeaders(originalRequest.url.encodedPath) || urlIsS3( + originalRequest.url.toString() + ) + ) { return chain.proceed(originalRequest) } @@ -36,13 +39,22 @@ class AuthInterceptor @Inject constructor( return response } + private fun urlIsS3(url: String): Boolean { + return url.contains(S3_URL) + } + + private fun shouldRequestAuthenticatedHeaders(encodedPath: String) = when (encodedPath) { - "/api/v1/members" -> false + "/api/v1/members" -> true // TODO : 프로필 수정과 URL이 겹쳐서 수정했습니다. "/api/v1/members/{accessToken}" -> false //TODO 추 후 변경 필요 else -> true } - private fun proceedWithAuthorizationHeader(chain: Interceptor.Chain, request: Request, token: String): Response { + private fun proceedWithAuthorizationHeader( + chain: Interceptor.Chain, + request: Request, + token: String, + ): Response { val newRequest = request.newBuilder() .addHeader("Authorization", token) .build() diff --git a/data/src/main/java/com/depromeet/data/mapper/HomeMapper.kt b/data/src/main/java/com/depromeet/data/mapper/HomeMapper.kt new file mode 100644 index 00000000..87014bcb --- /dev/null +++ b/data/src/main/java/com/depromeet/data/mapper/HomeMapper.kt @@ -0,0 +1,19 @@ +package com.depromeet.data.mapper + +import com.depromeet.data.model.response.home.ResponseBaseballTeamDto +import com.depromeet.data.model.response.home.ResponsePresignedUrlDto +import com.depromeet.domain.entity.response.home.BaseballTeamResponse +import com.depromeet.domain.entity.response.home.PresignedUrlResponse + + +/** baseball team **/ +fun ResponseBaseballTeamDto.toBaseballTeamResponse() = BaseballTeamResponse( + id = id, + name = name, + logo = logo +) + +/** PresignedUrl Mapper **/ +fun ResponsePresignedUrlDto.toPresignedUrlResponse() = PresignedUrlResponse( + presignedUrl = presignedUrl +) diff --git a/data/src/main/java/com/depromeet/data/model/request/home/RequestFileExtensionDto.kt b/data/src/main/java/com/depromeet/data/model/request/home/RequestFileExtensionDto.kt new file mode 100644 index 00000000..8163165c --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/request/home/RequestFileExtensionDto.kt @@ -0,0 +1,10 @@ +package com.depromeet.data.model.request.home + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestFileExtensionDto( + @SerialName("fileExtension") + val fileExtension: String, +) diff --git a/data/src/main/java/com/depromeet/data/model/request/home/RequestMySeatRecordDto.kt b/data/src/main/java/com/depromeet/data/model/request/home/RequestMySeatRecordDto.kt new file mode 100644 index 00000000..3dc6e842 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/request/home/RequestMySeatRecordDto.kt @@ -0,0 +1,27 @@ +package com.depromeet.data.model.request.home + +import com.depromeet.domain.entity.request.home.MySeatRecordRequest +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestMySeatRecordDto( + @SerialName("page") + val page: Int? = null, + @SerialName("size") + val size: Int? = null, + @SerialName("year") + val year: Int? = null, + @SerialName("month") + val month: Int? = null, +) { + companion object { + fun MySeatRecordRequest.toMySeatRecordRequestDto() = RequestMySeatRecordDto( + page = page, + size = size, + year = year, + month = month + ) + } +} + diff --git a/data/src/main/java/com/depromeet/data/model/request/home/RequestProfileEditDto.kt b/data/src/main/java/com/depromeet/data/model/request/home/RequestProfileEditDto.kt new file mode 100644 index 00000000..5ce92741 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/request/home/RequestProfileEditDto.kt @@ -0,0 +1,23 @@ +package com.depromeet.data.model.request.home + +import com.depromeet.domain.entity.request.home.ProfileEditRequest +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestProfileEditDto( + @SerialName("profileImage") + val profileImage: String? = null, + @SerialName("nickname") + val nickname: String? = null, + @SerialName("teamId") + val teamId: Int? = null, +) { + companion object { + fun ProfileEditRequest.toProfileEditRequestDto() = RequestProfileEditDto( + profileImage = url.ifEmpty { null }, + nickname = nickname.ifEmpty { null }, + teamId = teamId.takeIf { it !=0 } + ) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponseBaseballTeamDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponseBaseballTeamDto.kt new file mode 100644 index 00000000..cb46627e --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponseBaseballTeamDto.kt @@ -0,0 +1,14 @@ +package com.depromeet.data.model.response.home + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseBaseballTeamDto( + @SerialName("id") + val id : Int, + @SerialName("name") + val name : String, + @SerialName("logo") + val logo : String +) diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponseDeleteReviewDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponseDeleteReviewDto.kt new file mode 100644 index 00000000..c8a591d4 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponseDeleteReviewDto.kt @@ -0,0 +1,17 @@ +package com.depromeet.data.model.response.home + +import com.depromeet.domain.entity.response.home.DeleteReviewResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseDeleteReviewDto( + @SerialName("deletedReviewId") + val deleteReviewId: Int, +) { + companion object { + fun ResponseDeleteReviewDto.toDeleteReviewResponse() = DeleteReviewResponse( + reviewId = deleteReviewId + ) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponseMySeatRecordDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponseMySeatRecordDto.kt new file mode 100644 index 00000000..f3e8150b --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponseMySeatRecordDto.kt @@ -0,0 +1,228 @@ +package com.depromeet.data.model.response.home + +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class ResponseMySeatRecordDto( + @SerialName("memberInfoOnMyReview") + val memberInfoOnMyReview: ResponseMemberDto, + @SerialName("reviews") + val reviews: List, + @SerialName("totalElements") + val totalElements: Int, + @SerialName("totalPages") + val totalPages: Int, + @SerialName("number") + val number: Int, + @SerialName("size") + val size: Int, + @SerialName("first") + val first: Boolean, + @SerialName("last") + val last: Boolean, + @SerialName("filter") + val filter: ResponseFilterDto, +) { + @Serializable + data class ResponseMemberDto( + @SerialName("userId") + val userId: Int, + @SerialName("profileImageUrl") + val profileImageUrl: String?, + @SerialName("level") + val level: Int, + @SerialName("levelTitle") + val levelTitle: String, + @SerialName("nickname") + val nickname: String, + @SerialName("reviewCount") + val reviewCount: Int, + ) + + + @Serializable + data class ResponseReviewWrapperDto( + @SerialName("baseReview") + val baseReview: ResponseReviewDto, + @SerialName("stadiumName") + val stadiumName: String, + @SerialName("sectionName") + val sectionName: String, + @SerialName("blockCode") + val blockCode: String, + ) + + @Serializable + data class ResponseReviewDto( + @SerialName("id") + val id: Int, + @SerialName("member") + val member: ResponseMemberDto, + @SerialName("stadium") + val stadium: ResponseStadiumDto, + @SerialName("section") + val section: ResponseSectionDto, + @SerialName("block") + val block: ResponseBlockDto, + @SerialName("row") + val row: ResponseRowDto, + @SerialName("seat") + val seat: ResponseSeatDto, + @SerialName("dateTime") + val dateTime: String, + @SerialName("content") + val content: String, + @SerialName("images") + val images: List, + @SerialName("keywords") + val keywords: List, + ) { + @Serializable + data class ResponseReviewKeywordDto( + @SerialName("id") + val id: Int, + @SerialName("content") + val content: String, + @SerialName("isPositive") + val isPositive: Boolean, + ) + + @Serializable + data class ResponseMemberDto( + @SerialName("profileImage") + val profileImage: String?, + @SerialName("nickname") + val nickname: String, + @SerialName("level") + val level: Int, + ) + + @Serializable + data class ResponseStadiumDto( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, + ) + + @Serializable + data class ResponseSectionDto( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, + @SerialName("alias") + val alias: String?, + ) + + @Serializable + data class ResponseBlockDto( + @SerialName("id") + val id: Int, + @SerialName("code") + val code: String, + ) + + @Serializable + data class ResponseRowDto( + @SerialName("id") + val id: Int, + @SerialName("number") + val number: Int, + ) + + @Serializable + data class ResponseSeatDto( + @SerialName("id") + val id: Int, + @SerialName("seatNumber") + val seatNumber: Int, + ) + + @Serializable + data class ResponseReviewImageDto( + @SerialName("id") + val id: Int, + @SerialName("url") + val url: String, + ) + } + + + @Serializable + data class ResponseFilterDto( + @SerialName("rowNumber") + val rowNumber: Int?, + @SerialName("seatNumber") + val seatNumber: Int?, + @SerialName("year") + val year: Int?, + @SerialName("month") + val month: Int?, + ) + + companion object { + fun ResponseMySeatRecordDto.toMySeatRecordResponse() = MySeatRecordResponse( + profile = memberInfoOnMyReview.toMyProfileResponse(), + reviews = reviews.map { it.baseReview.toReviewResponse() }, + totalElements = totalElements, + totalPages = totalPages, + number = number, + size = size, + first = first, + last = last, + isLoading = false + ) + + private fun ResponseMemberDto.toMyProfileResponse() = + MySeatRecordResponse.MyProfileResponse( + userId = userId, + profileImage = profileImageUrl ?: "", + level = level, + levelTitle = levelTitle, + nickname = nickname, + reviewCount = reviewCount + ) + + private fun ResponseReviewDto.toReviewResponse() = MySeatRecordResponse.ReviewResponse( + id = id, + stadiumId = stadium.id, + stadiumName = stadium.name, + blockId = block.id, + blockName = block.code, + seatId = seat.id, + rowId = row.id, + rowNumber = row.number, + seatNumber = seat.seatNumber, + date = dateTime, + content = content, + sectionName = section.name, + member = member.toMemberResponse(), + images = images.map { it.toReviewImageResponse() }, + keywords = keywords.map { it.toReviewKeywordResponse() } + ) + + private fun ResponseReviewDto.ResponseMemberDto.toMemberResponse() = + MySeatRecordResponse.ReviewResponse.MemberResponse( + profileImage = profileImage ?: "", + nickname = nickname, + level = level + ) + + private fun ResponseReviewDto.ResponseReviewKeywordDto.toReviewKeywordResponse() = + MySeatRecordResponse.ReviewResponse.ReviewKeywordResponse( + id = id, + content = content, + isPositive = isPositive + ) + + private fun ResponseReviewDto.ResponseReviewImageDto.toReviewImageResponse() = + MySeatRecordResponse.ReviewResponse.ReviewImageResponse( + id = id, + url = url + ) + } +} diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponsePresignedUrlDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponsePresignedUrlDto.kt new file mode 100644 index 00000000..c673ce56 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponsePresignedUrlDto.kt @@ -0,0 +1,10 @@ +package com.depromeet.data.model.response.home + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePresignedUrlDto( + @SerialName("presignedUrl") + val presignedUrl : String +) diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponseProfileDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponseProfileDto.kt new file mode 100644 index 00000000..67d3e14f --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponseProfileDto.kt @@ -0,0 +1,32 @@ +package com.depromeet.data.model.response.home + +import com.depromeet.domain.entity.response.home.ProfileResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseProfileDto( + @SerialName("teamId") + val teamId : Int, + @SerialName("profileImageUrl") + val profileImageUrl: String?, + @SerialName("nickname") + val nickname: String, + @SerialName("level") + val level: Int, + @SerialName("levelTitle") + val levelTitle: String, + @SerialName("teamImageUrl") + val teamImageUrl: String?, +) { + companion object { + fun ResponseProfileDto.toProfileResponse() = ProfileResponse( + teamId = teamId, + profileImage = profileImageUrl ?: "", + nickname = nickname, + level = level, + levelTitle = levelTitle, + teamImage = teamImageUrl ?: "", + ) + } +} diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponseProfileEditDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponseProfileEditDto.kt new file mode 100644 index 00000000..cf5e017b --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponseProfileEditDto.kt @@ -0,0 +1,26 @@ +package com.depromeet.data.model.response.home + +import com.depromeet.domain.entity.response.home.ProfileEditResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseProfileEditDto( + @SerialName("id") + val id: Int, + @SerialName("nickname") + val nickname: String, + @SerialName("teamId") + val teamId: Int, +) { + companion object { + fun ResponseProfileEditDto.toProfileEditResponse(): ProfileEditResponse { + return ProfileEditResponse( + id = id, + nickname = nickname, + teamId = teamId + ) + } + } + +} \ No newline at end of file diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponseRecentReviewDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponseRecentReviewDto.kt new file mode 100644 index 00000000..7cfcfbc9 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponseRecentReviewDto.kt @@ -0,0 +1,193 @@ +package com.depromeet.data.model.response.home + +import com.depromeet.domain.entity.response.home.RecentReviewResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseRecentReviewDto( + @SerialName("review") + val review: ResponseReviewWrapperDto, + @SerialName("reviewCount") + val reviewCount: Int, +) { + @Serializable + data class ResponseReviewWrapperDto( + @SerialName("baseReview") + val baseReview: ResponseReviewDto, + @SerialName("stadiumName") + val stadiumName: String, + @SerialName("sectionName") + val sectionName: String, + @SerialName("blockCode") + val blockCode: String, + ) + + @Serializable + data class ResponseReviewDto( + @SerialName("id") + val id: Int, + @SerialName("member") + val member: ResponseMemberDto, + @SerialName("stadium") + val stadium: ResponseStadiumDto, + @SerialName("section") + val section: ResponseSectionDto, + @SerialName("block") + val block: ResponseBlockDto, + @SerialName("row") + val row: ResponseRowDto, + @SerialName("seat") + val seat: ResponseSeatDto, + @SerialName("dateTime") + val dateTime: String, + @SerialName("content") + val content: String, + @SerialName("images") + val images: List, + @SerialName("keywords") + val keywords: List, + ) + + @Serializable + data class ResponseMemberDto( + @SerialName("profileImage") + val profileImage: String?, + @SerialName("nickname") + val nickname: String, + @SerialName("level") + val level: Int, + ) + + @Serializable + data class ResponseStadiumDto( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, + ) + + @Serializable + data class ResponseSectionDto( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, + @SerialName("alias") + val alias: String?, + ) + + @Serializable + data class ResponseBlockDto( + @SerialName("id") + val id: Int, + @SerialName("code") + val code: String, + ) + + @Serializable + data class ResponseRowDto( + @SerialName("id") + val id: Int, + @SerialName("number") + val number: Int, + ) + + @Serializable + data class ResponseSeatDto( + @SerialName("id") + val id: Int, + @SerialName("seatNumber") + val seatNumber: Int, + ) + + @Serializable + data class ResponseImageDto( + @SerialName("id") + val id: Int, + @SerialName("url") + val url: String, + ) + + @Serializable + data class ResponseKeywordDto( + @SerialName("id") + val id: Int, + @SerialName("content") + val content: String, + @SerialName("isPositive") + val isPositive: Boolean, + ) + + companion object { + fun ResponseRecentReviewDto.toRecentReviewResponse() = RecentReviewResponse( + review = review.toReviewWrapperResponse(), + reviewCount = reviewCount + ) + + fun ResponseReviewWrapperDto.toReviewWrapperResponse() = + RecentReviewResponse.ReviewWrapperResponse( + baseReview = baseReview.toReviewResponse(), + stadiumName = stadiumName, + sectionName = sectionName, + blockCode = blockCode + ) + + fun ResponseReviewDto.toReviewResponse() = RecentReviewResponse.ReviewResponse( + id = id, + member = member.toMemberResponse(), + stadium = stadium.toStadiumResponse(), + section = section.toSectionResponse(), + block = block.toBlockResponse(), + row = row.toRowResponse(), + seat = seat.toSeatResponse(), + dateTime = dateTime, + content = content, + images = images.map { it.toImageResponse() }, + keywords = keywords.map { it.toKeywordResponse() } + ) + + fun ResponseMemberDto.toMemberResponse() = RecentReviewResponse.MemberResponse( + profileImage = profileImage, + nickname = nickname, + level = level + ) + + fun ResponseStadiumDto.toStadiumResponse() = RecentReviewResponse.StadiumResponse( + id = id, + name = name + ) + + fun ResponseSectionDto.toSectionResponse() = RecentReviewResponse.SectionResponse( + id = id, + name = name, + alias = alias + ) + + fun ResponseBlockDto.toBlockResponse() = RecentReviewResponse.BlockResponse( + id = id, + code = code + ) + + fun ResponseRowDto.toRowResponse() = RecentReviewResponse.RowResponse( + id = id, + number = number + ) + + fun ResponseSeatDto.toSeatResponse() = RecentReviewResponse.SeatResponse( + id = id, + seatNumber = seatNumber + ) + + fun ResponseImageDto.toImageResponse() = RecentReviewResponse.ImageResponse( + id = id, + url = url + ) + + fun ResponseKeywordDto.toKeywordResponse() = RecentReviewResponse.KeywordResponse( + id = id, + content = content, + isPositive = isPositive + ) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/depromeet/data/model/response/home/ResponseReviewDateDto.kt b/data/src/main/java/com/depromeet/data/model/response/home/ResponseReviewDateDto.kt new file mode 100644 index 00000000..41bc4fe3 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/home/ResponseReviewDateDto.kt @@ -0,0 +1,32 @@ +package com.depromeet.data.model.response.home + +import com.depromeet.domain.entity.response.home.ReviewDateResponse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseReviewDateDto( + @SerialName("yearMonths") + val yearMonths: List, +) { + @Serializable + data class ResponseDateDto( + @SerialName("year") + val year: Int, + @SerialName("month") + val month: Int, + ) + + companion object { + fun ResponseReviewDateDto.toReviewDateResponse(): ReviewDateResponse { + val groupedByYear = this.yearMonths.groupBy { it.year } + val yearMonths = groupedByYear.map { (year, dates) -> + ReviewDateResponse.YearMonths( + year = year, + months = listOf(0) + dates.map { it.month }.sorted() + ) + }.sortedByDescending { it.year } + return ReviewDateResponse(yearMonths = yearMonths) + } + } +} diff --git a/data/src/main/java/com/depromeet/data/remote/HomeApiService.kt b/data/src/main/java/com/depromeet/data/remote/HomeApiService.kt new file mode 100644 index 00000000..19b12fca --- /dev/null +++ b/data/src/main/java/com/depromeet/data/remote/HomeApiService.kt @@ -0,0 +1,69 @@ +package com.depromeet.data.remote + +import com.depromeet.data.model.request.home.RequestFileExtensionDto +import com.depromeet.data.model.request.home.RequestProfileEditDto +import com.depromeet.data.model.response.home.ResponseBaseballTeamDto +import com.depromeet.data.model.response.home.ResponseDeleteReviewDto +import com.depromeet.data.model.response.home.ResponseMySeatRecordDto +import com.depromeet.data.model.response.home.ResponsePresignedUrlDto +import com.depromeet.data.model.response.home.ResponseProfileDto +import com.depromeet.data.model.response.home.ResponseProfileEditDto +import com.depromeet.data.model.response.home.ResponseRecentReviewDto +import com.depromeet.data.model.response.home.ResponseReviewDateDto +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.Url + +interface HomeApiService { + @GET("/api/v1/reviews") + suspend fun getMySeatRecord( + @Query("page") page: Int?, + @Query("size") size: Int?, + @Query("year") year: Int?, + @Query("month") month: Int?, + ): ResponseMySeatRecordDto + + @GET("/api/v1/baseball-teams") + suspend fun getBaseballTeam(): List + + @POST("/api/v1/members/profile/images") + suspend fun postProfileImagePresigned( + @Body body: RequestFileExtensionDto, + ): ResponsePresignedUrlDto + + @PUT + suspend fun putProfileImage( + @Url preSignedUrl: String, + @Body image: RequestBody, + ) + + @PUT("/api/v1/members") + suspend fun putProfileEdit( + @Body body: RequestProfileEditDto, + ): ResponseProfileEditDto + + @GET("/api/v1/members/duplicatedNickname/{nickname}") + suspend fun getDuplicateNickname( + @Path("nickname") nickname: String, + ) + + @GET("/api/v1/reviews/months") + suspend fun getReviewDate(): ResponseReviewDateDto + + @GET("/api/v1/members/memberInfo") + suspend fun getProfileInfo(): ResponseProfileDto + + @GET("/api/v1/reviews/recentReview") + suspend fun getRecentReview(): ResponseRecentReviewDto + + @DELETE("/api/v1/reviews/{reviewId}") + suspend fun deleteReview( + @Path("reviewId") reviewId: Int, + ): ResponseDeleteReviewDto +} \ No newline at end of file diff --git a/data/src/main/java/com/depromeet/data/repository/HomeRepositoryImpl.kt b/data/src/main/java/com/depromeet/data/repository/HomeRepositoryImpl.kt new file mode 100644 index 00000000..0cc6fd70 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/repository/HomeRepositoryImpl.kt @@ -0,0 +1,96 @@ +package com.depromeet.data.repository + +import com.depromeet.data.datasource.HomeDataSource +import com.depromeet.data.mapper.toBaseballTeamResponse +import com.depromeet.data.mapper.toPresignedUrlResponse +import com.depromeet.data.model.request.home.RequestMySeatRecordDto.Companion.toMySeatRecordRequestDto +import com.depromeet.data.model.request.home.RequestProfileEditDto.Companion.toProfileEditRequestDto +import com.depromeet.data.model.response.home.ResponseDeleteReviewDto.Companion.toDeleteReviewResponse +import com.depromeet.data.model.response.home.ResponseMySeatRecordDto.Companion.toMySeatRecordResponse +import com.depromeet.data.model.response.home.ResponseProfileDto.Companion.toProfileResponse +import com.depromeet.data.model.response.home.ResponseProfileEditDto.Companion.toProfileEditResponse +import com.depromeet.data.model.response.home.ResponseRecentReviewDto.Companion.toRecentReviewResponse +import com.depromeet.data.model.response.home.ResponseReviewDateDto.Companion.toReviewDateResponse +import com.depromeet.domain.entity.request.home.MySeatRecordRequest +import com.depromeet.domain.entity.request.home.ProfileEditRequest +import com.depromeet.domain.entity.response.home.BaseballTeamResponse +import com.depromeet.domain.entity.response.home.DeleteReviewResponse +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import com.depromeet.domain.entity.response.home.PresignedUrlResponse +import com.depromeet.domain.entity.response.home.ProfileEditResponse +import com.depromeet.domain.entity.response.home.ProfileResponse +import com.depromeet.domain.entity.response.home.RecentReviewResponse +import com.depromeet.domain.entity.response.home.ReviewDateResponse +import com.depromeet.domain.repository.HomeRepository +import javax.inject.Inject + +class HomeRepositoryImpl @Inject constructor( + private val homeDataSource: HomeDataSource, +) : HomeRepository { + override suspend fun getMySeatRecord(mySeatRecordRequest: MySeatRecordRequest): Result { + return runCatching { + homeDataSource.getMySeatRecordData(mySeatRecordRequest.toMySeatRecordRequestDto()) + .toMySeatRecordResponse() + } + } + + override suspend fun getBaseballTeam(): Result> { + return runCatching { + homeDataSource.getBaseballTeamData().map { it.toBaseballTeamResponse() } + } + } + + override suspend fun postProfileImagePresigned( + fileExtension: String, + ): Result { + return runCatching { + homeDataSource.postProfileImagePresigned(fileExtension) + .toPresignedUrlResponse() + } + } + + override suspend fun putProfileImage(presignedUrl: String, image: ByteArray): Result { + return runCatching { + homeDataSource.putProfileImage(presignedUrl, image) + } + } + + override suspend fun putProfileEdit( + profileEditRequest: ProfileEditRequest, + ): Result { + return runCatching { + homeDataSource.putProfileEdit(profileEditRequest.toProfileEditRequestDto()) + .toProfileEditResponse() + } + } + + override suspend fun getDuplicateNickname(nickname: String): Result { + return runCatching { + homeDataSource.getDuplicateNickname(nickname) + } + } + + override suspend fun getReviewDate(): Result { + return runCatching { + homeDataSource.getReviewDate().toReviewDateResponse() + } + } + + override suspend fun getProfile(): Result { + return runCatching { + homeDataSource.getProfile().toProfileResponse() + } + } + + override suspend fun getRecentReview(): Result { + return runCatching { + homeDataSource.getRecentReview().toRecentReviewResponse() + } + } + + override suspend fun deleteReview(reviewId: Int): Result { + return runCatching { + homeDataSource.deleteReview(reviewId).toDeleteReviewResponse() + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/depromeet/domain/entity/request/home/MySeatRecordRequest.kt b/domain/src/main/java/com/depromeet/domain/entity/request/home/MySeatRecordRequest.kt new file mode 100644 index 00000000..32f08859 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/request/home/MySeatRecordRequest.kt @@ -0,0 +1,10 @@ +package com.depromeet.domain.entity.request.home + +data class MySeatRecordRequest( + val page: Int ?= null, + val size: Int ?= 10, + val year: Int ?= 2024, + val month: Int ?= null, +) + + diff --git a/domain/src/main/java/com/depromeet/domain/entity/request/home/ProfileEditRequest.kt b/domain/src/main/java/com/depromeet/domain/entity/request/home/ProfileEditRequest.kt new file mode 100644 index 00000000..d574239f --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/request/home/ProfileEditRequest.kt @@ -0,0 +1,7 @@ +package com.depromeet.domain.entity.request.home + +data class ProfileEditRequest( + val url : String = "", + val nickname : String = "", + val teamId : Int = 0, +) diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/BaseballTeamResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/BaseballTeamResponse.kt new file mode 100644 index 00000000..5d7e859d --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/BaseballTeamResponse.kt @@ -0,0 +1,8 @@ +package com.depromeet.domain.entity.response.home + +data class BaseballTeamResponse( + val id : Int, + val name : String = "", + val logo : String = "", + val isClicked : Boolean = false +) diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/DeleteReviewResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/DeleteReviewResponse.kt new file mode 100644 index 00000000..331bdc42 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/DeleteReviewResponse.kt @@ -0,0 +1,5 @@ +package com.depromeet.domain.entity.response.home + +data class DeleteReviewResponse( + val reviewId : Int +) diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/MySeatRecordResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/MySeatRecordResponse.kt new file mode 100644 index 00000000..fa3ed706 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/MySeatRecordResponse.kt @@ -0,0 +1,58 @@ +package com.depromeet.domain.entity.response.home + +data class MySeatRecordResponse( + val profile: MyProfileResponse = MyProfileResponse(), + val reviews: List = emptyList(), + val totalElements: Int = 0, + val totalPages: Int = 0, + val number: Int = 0, + val size: Int = 0, + val first : Boolean = true, + val last : Boolean = true, + val isLoading : Boolean = true, +) { + data class MyProfileResponse( + val userId: Int = 0, + val profileImage: String = "", + val level: Int = 0, + val levelTitle : String = "", + val nickname: String = "", + val reviewCount: Int = 0, + ) + + data class ReviewResponse( + val id: Int, + val stadiumId: Int, + val stadiumName: String = "", + val blockId: Int = 0, + val blockName: String = "", + val seatId: Int = 0, + val rowId: Int = 0, + val rowNumber: Int = 0, + val seatNumber: Int = 0, + val date: String = "", + val content: String = "", + val sectionName : String = "", + val member: MemberResponse = MemberResponse(), + val images: List = emptyList(), + val keywords: List = emptyList(), + ) { + data class ReviewImageResponse( + val id: Int = 0, + val url: String = "", + ) + + data class ReviewKeywordResponse( + val id: Int = 0, + val content: String = "", + val isPositive: Boolean = false, + ) + + data class MemberResponse( + val profileImage: String = "", + val nickname: String = "", + val level: Int = 0, + ) + } + +} diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/PresignedUrlResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/PresignedUrlResponse.kt new file mode 100644 index 00000000..691502a3 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/PresignedUrlResponse.kt @@ -0,0 +1,5 @@ +package com.depromeet.domain.entity.response.home + +data class PresignedUrlResponse( + val presignedUrl: String = "" +) \ No newline at end of file diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/ProfileEditResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/ProfileEditResponse.kt new file mode 100644 index 00000000..c746a840 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/ProfileEditResponse.kt @@ -0,0 +1,7 @@ +package com.depromeet.domain.entity.response.home + +data class ProfileEditResponse( + val id: Int, + val nickname: String = "", + val teamId: Int = 0, +) \ No newline at end of file diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/ProfileResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/ProfileResponse.kt new file mode 100644 index 00000000..e48ff839 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/ProfileResponse.kt @@ -0,0 +1,10 @@ +package com.depromeet.domain.entity.response.home + +data class ProfileResponse( + val teamId : Int = 0, + val profileImage : String = "", + val nickname :String = "", + val level : Int = 0, + val levelTitle : String = "", + val teamImage : String = "" +) \ No newline at end of file diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/RecentReviewResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/RecentReviewResponse.kt new file mode 100644 index 00000000..3b715a81 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/RecentReviewResponse.kt @@ -0,0 +1,71 @@ +package com.depromeet.domain.entity.response.home + +data class RecentReviewResponse( + val review: ReviewWrapperResponse, + val reviewCount: Int = 0, +) { + data class ReviewWrapperResponse( + val baseReview: ReviewResponse, + val stadiumName: String = "", + val sectionName: String = "", + val blockCode: String = "", + ) + + data class ReviewResponse( + val id: Int, + val member: MemberResponse = MemberResponse(), + val stadium: StadiumResponse, + val section: SectionResponse, + val block: BlockResponse, + val row: RowResponse, + val seat: SeatResponse, + val dateTime: String = "", + val content: String = "", + val images: List = emptyList(), + val keywords: List = emptyList(), + ) + + data class MemberResponse( + val profileImage: String? = "", + val nickname: String = "", + val level: Int = 0, + ) + + data class StadiumResponse( + val id: Int, + val name: String = "", + ) + + data class SectionResponse( + val id: Int, + val name: String = "", + val alias: String? = "", + ) + + data class BlockResponse( + val id: Int, + val code: String = "", + ) + + data class RowResponse( + val id: Int, + val number: Int = 0, + ) + + data class SeatResponse( + val id: Int, + val seatNumber: Int = 0, + ) + + data class ImageResponse( + val id: Int, + val url: String = "", + ) + + data class KeywordResponse( + val id: Int = 0, + val content: String = "", + val isPositive: Boolean = false, + ) + +} \ No newline at end of file diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/home/ReviewDateResponse.kt b/domain/src/main/java/com/depromeet/domain/entity/response/home/ReviewDateResponse.kt new file mode 100644 index 00000000..f8de84f9 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/home/ReviewDateResponse.kt @@ -0,0 +1,10 @@ +package com.depromeet.domain.entity.response.home + +data class ReviewDateResponse( + val yearMonths: List = emptyList(), +) { + data class YearMonths( + val year: Int = 2024, + val months: List = emptyList(), + ) +} diff --git a/domain/src/main/java/com/depromeet/domain/repository/HomeRepository.kt b/domain/src/main/java/com/depromeet/domain/repository/HomeRepository.kt new file mode 100644 index 00000000..096a71a9 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/repository/HomeRepository.kt @@ -0,0 +1,47 @@ +package com.depromeet.domain.repository + +import com.depromeet.domain.entity.request.home.MySeatRecordRequest +import com.depromeet.domain.entity.request.home.ProfileEditRequest +import com.depromeet.domain.entity.response.home.BaseballTeamResponse +import com.depromeet.domain.entity.response.home.DeleteReviewResponse +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import com.depromeet.domain.entity.response.home.PresignedUrlResponse +import com.depromeet.domain.entity.response.home.ProfileEditResponse +import com.depromeet.domain.entity.response.home.ProfileResponse +import com.depromeet.domain.entity.response.home.RecentReviewResponse +import com.depromeet.domain.entity.response.home.ReviewDateResponse + +interface HomeRepository { + suspend fun getMySeatRecord( + mySeatRecordRequest: MySeatRecordRequest, + ): Result + + suspend fun getBaseballTeam(): Result> + + suspend fun postProfileImagePresigned( + fileExtension: String, + ): Result + + suspend fun putProfileImage( + presignedUrl: String, + image: ByteArray, + ): Result + + suspend fun putProfileEdit( + profileEditRequest: ProfileEditRequest, + ): Result + + suspend fun getDuplicateNickname( + nickname: String, + ): Result + + suspend fun getReviewDate(): Result + + suspend fun getProfile(): Result + + suspend fun getRecentReview(): Result + + suspend fun deleteReview( + reviewId: Int, + ): Result +} \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/extension/StringExt.kt b/presentation/src/main/java/com/depromeet/presentation/extension/StringExt.kt index 2e8a3035..7ae0e581 100644 --- a/presentation/src/main/java/com/depromeet/presentation/extension/StringExt.kt +++ b/presentation/src/main/java/com/depromeet/presentation/extension/StringExt.kt @@ -1,59 +1,13 @@ package com.depromeet.presentation.extension -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import java.time.format.TextStyle -import java.util.Locale +import com.depromeet.presentation.login.viewmodel.NicknameInputState const val NICKNAME_PATTERN = "^[a-zA-Z0-9가-힣]+$" -const val TEST_DUPLICATE_NICKNAME = "안드로이드" //서버 연동되면 삭제예정 -sealed class NickNameError { - object NoError : NickNameError() - object LengthError : NickNameError() - object InvalidCharacterError : NickNameError() - object DuplicateError : NickNameError() -} - - -fun String.validateNickName(): NickNameError { +fun String.validateNickName(): NicknameInputState { return when { - this.length !in 2..10 -> NickNameError.LengthError - !this.matches(Regex(NICKNAME_PATTERN)) -> NickNameError.InvalidCharacterError - this == TEST_DUPLICATE_NICKNAME -> NickNameError.DuplicateError - else -> NickNameError.NoError - } -} - -/** ex) 2024-01-21 (서버 date형식 예정)*/ -fun String.extractDay(): String { - val parts = this.split("-") - return if (parts.size == 3) { - parts[2] - } else { - "" + this.length !in 2..10 -> NicknameInputState.INVALID_LENGTH + !this.matches(Regex(NICKNAME_PATTERN)) -> NicknameInputState.INVALID_CHARACTER + else -> NicknameInputState.VALID } -} - -fun String.extractMonth(includeSuffixMonth: Boolean): String { - val parts = this.split("-") - if (parts.size == 3) { - val month = parts[1].toIntOrNull()?.toString() ?: return "" - return if (includeSuffixMonth) "${month}월" else month - } else if (this.length <= 2) { - return this.toIntOrNull()?.toString() ?: return "" - } - return "" -} - -//요일 확인 -fun String.getDayOfWeek(): String { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val date = LocalDate.parse(this, formatter) - return date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) -} - -fun String.getYearMonthDay(): String { - return LocalDate.parse(this, DateTimeFormatter.ISO_LOCAL_DATE) - .format(DateTimeFormatter.ofPattern("yyyy년 M월 d일")) } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/extension/ViewExt.kt b/presentation/src/main/java/com/depromeet/presentation/extension/ViewExt.kt index 58d1e66e..a98e45f9 100644 --- a/presentation/src/main/java/com/depromeet/presentation/extension/ViewExt.kt +++ b/presentation/src/main/java/com/depromeet/presentation/extension/ViewExt.kt @@ -4,6 +4,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import coil.load +import com.depromeet.presentation.R inline fun View.setOnSingleClickListener( delay: Long = 1000L, @@ -27,7 +28,10 @@ fun View.setMargins(left: Int, top: Int, right: Int, bottom: Int) { } } -fun ImageView.loadAndClip(imageUrl: String) { - this.load(imageUrl) +fun ImageView.loadAndClip(imageUrl: Any) { + this.load(imageUrl){ + placeholder(R.drawable.ic_image_placeholder) + error(R.drawable.ic_image_error) + } this.clipToOutline = true } diff --git a/presentation/src/main/java/com/depromeet/presentation/home/HomeActivity.kt b/presentation/src/main/java/com/depromeet/presentation/home/HomeActivity.kt index 45cf890a..b4543d6d 100644 --- a/presentation/src/main/java/com/depromeet/presentation/home/HomeActivity.kt +++ b/presentation/src/main/java/com/depromeet/presentation/home/HomeActivity.kt @@ -1,26 +1,42 @@ package com.depromeet.presentation.home +import android.app.Activity import android.content.Intent import android.os.Bundle import android.text.SpannableStringBuilder import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.lifecycle.asLiveData import coil.load +import coil.transform.CircleCropTransformation import com.depromeet.core.base.BaseActivity import com.depromeet.core.state.UiState +import com.depromeet.domain.entity.response.home.ProfileResponse +import com.depromeet.domain.entity.response.home.RecentReviewResponse import com.depromeet.presentation.databinding.ActivityHomeBinding import com.depromeet.presentation.extension.loadAndClip import com.depromeet.presentation.extension.toast -import com.depromeet.presentation.home.viewmodel.HomeUiState import com.depromeet.presentation.home.viewmodel.HomeViewModel +import com.depromeet.presentation.seatReview.ReviewActivity +import com.depromeet.presentation.seatrecord.SeatRecordActivity +import com.depromeet.presentation.util.CalendarUtil import com.depromeet.presentation.util.applyBoldAndSizeSpan +import com.depromeet.presentation.viewfinder.StadiumActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class HomeActivity : BaseActivity( ActivityHomeBinding::inflate ) { + companion object { + const val PROFILE_NAME = "profile_name" + const val PROFILE_IMAGE = "profile_image" + const val PROFILE_CHEER_TEAM = "profile_cheer_team" + } + private val viewModel: HomeViewModel by viewModels() private val sightList: List by lazy { listOf( @@ -31,84 +47,142 @@ class HomeActivity : BaseActivity( ) } + private val editProfileLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + val nickname = data?.getStringExtra(ProfileEditActivity.PROFILE_NAME) ?: "" + val profileImage = data?.getStringExtra(ProfileEditActivity.PROFILE_IMAGE) ?: "" + val teamId = data?.getIntExtra(ProfileEditActivity.PROFILE_CHEER_TEAM, 0) ?: 0 + val teamIdUrl = + data?.getStringExtra(ProfileEditActivity.PROFILE_CHEER_TEAM_URL) ?: "" + + viewModel.updateTest(nickname, profileImage, teamId, teamIdUrl) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding.ivHomeProfile.setOnClickListener { navigateToProfileEditActivity() } - binding.ibHomeEdit.setOnClickListener { navigateToProfileEditActivity() } - binding.clMainSight.clipToOutline = true + viewModel.getInformation() + initView() + initEvent() + initObserver() + + } + + private fun initView() { + binding.clMainFindSight.clipToOutline = true + } + - viewModel.uiState.asLiveData().observe(this) { state -> + private fun initEvent() = with(binding) { + + /** 프로필 수정 */ + ivHomeProfile.setOnClickListener { navigateToProfileEditActivity() } + ibHomeEdit.setOnClickListener { navigateToProfileEditActivity() } + /** 내 시야 기록 */ + clHomeSightRecord.setOnClickListener { navigateToSeatRecordActivity() } + tvHomeMore.setOnClickListener { navigateToSeatRecordActivity() } + clHomeOneRecord.setOnClickListener { navigateToSeatRecordActivity() } + clHomeTwoRecord.setOnClickListener { navigateToSeatRecordActivity() } + clHomeThreeRecord.setOnClickListener { navigateToSeatRecordActivity() } + /** 시야후기 등록*/ + clHomeRegisterSight.setOnClickListener { navigateToReviewActivity() } + clHomeNoRecord.setOnClickListener { navigateToReviewActivity() } + /** 시야 찾기 */ + clMainFindSight.setOnClickListener { navigateToStadiumActivity() } + + ibHomeSetting.setOnClickListener { /** 셋팅 이동 **/ } + + } + + private fun initObserver() { + viewModel.profile.asLiveData().observe(this) { state -> when (state) { - is UiState.Loading -> { - toast("로딩중") + is UiState.Success -> { + updateProfile(state.data) } - is UiState.Empty -> { - toast("빈값") + is UiState.Failure -> { + toast("실패") } + is UiState.Loading -> {} + is UiState.Empty -> {} + } + } + viewModel.recentReview.asLiveData().observe(this) { state -> + when (state) { is UiState.Success -> { - updateUi(state.data) + updateRecentReview(state.data) } is UiState.Failure -> { - toast("통신 실패") + toast("실패") } + + is UiState.Loading -> {} + is UiState.Empty -> {} } } - - viewModel.getInformation() } - - private fun updateUi(state: HomeUiState) { - with(binding) { - val profile = state.profile - val recentSight = state.recentSight - - "Lv.${profile.level} ${profile.title}".also { tvHomeLevel.text = it } - setSpannableString(profile.nickName, profile.writeCount) - ivHomeCheerTeam.load(profile.cheerTeam) - tvHomeRecentRecordName.text = recentSight.location - tvHomeRecentRecordDate.text = recentSight.date - + private fun updateRecentReview(data: RecentReviewResponse) = with(binding) { + setSpannableString(viewModel.nickname.value, viewModel.reviewCount.value) + tvHomeRecentRecordName.text = data.review.stadiumName + tvHomeRecentRecordDate.text = CalendarUtil.getFormattedDate(data.review.baseReview.dateTime) - sightList.forEachIndexed { index, view -> - view.visibility = - if (index == recentSight.photoList.size) View.VISIBLE else View.GONE + sightList.forEachIndexed { index, view -> + view.visibility = + if (index == data.review.baseReview.images.size) VISIBLE else GONE + } + when (data.review.baseReview.images.size) { + 0 -> { + tvHomeRecentRecordDate.visibility = GONE + tvHomeRecentRecordName.visibility = GONE } - when (recentSight.photoList.size) { - 0 -> { - tvHomeRecentRecordDate.visibility = View.GONE - tvHomeRecentRecordName.visibility = View.GONE + 1 -> { + ivHomeRecentOneRecord1.loadAndClip(data.review.baseReview.images[0]) + "${data.review.sectionName} ${data.review.blockCode}블록".also { + tvHomeRecentOneSection.text = it } + } - 1 -> { - ivHomeRecentOneRecord1.loadAndClip(recentSight.photoList[0]) - tvHomeRecentOneSection.text = recentSight.section + 2 -> { + ivHomeRecentTwoRecord1.loadAndClip(data.review.baseReview.images[0]) + ivHomeRecentTwoRecord2.loadAndClip(data.review.baseReview.images[1]) + "${data.review.sectionName} ${data.review.blockCode}블록".also { + tvHomeRecentOneSection.text = it } + } - 2 -> { - ivHomeRecentTwoRecord1.loadAndClip(recentSight.photoList[0]) - ivHomeRecentTwoRecord2.loadAndClip(recentSight.photoList[1]) - tvHomeRecentTwoSection.text = recentSight.section + 3 -> { + ivHomeRecentThreeRecord1.loadAndClip(data.review.baseReview.images[0]) + ivHomeRecentThreeRecord2.loadAndClip(data.review.baseReview.images[1]) + ivHomeRecentThreeRecord3.loadAndClip(data.review.baseReview.images[2]) + "${data.review.sectionName} ${data.review.blockCode}블록".also { + tvHomeRecentOneSection.text = it } + } - 3 -> { - ivHomeRecentThreeRecord1.loadAndClip(recentSight.photoList[0]) - ivHomeRecentThreeRecord2.loadAndClip(recentSight.photoList[1]) - ivHomeRecentThreeRecord3.loadAndClip(recentSight.photoList[2]) - tvHomeRecentThreeSection.text = recentSight.section - } + else -> {} + } + } - else -> {} + private fun updateProfile(profile: ProfileResponse) = with(binding) { + setSpannableString(viewModel.nickname.value, viewModel.reviewCount.value) + if (profile.profileImage.isEmpty()) { + ivHomeProfile.setImageResource(com.depromeet.presentation.R.drawable.ic_default_profile) + } else { + ivHomeProfile.load(profile.profileImage) { + transformations(CircleCropTransformation()) } } - } + "Lv.${profile.level} ${profile.levelTitle}".also { tvHomeLevel.text = it } + ivHomeCheerTeam.load(profile.teamImage) - private fun navigateToProfileEditActivity() { - Intent(this, ProfileEditActivity::class.java).apply { startActivity(this) } } private fun setSpannableString( @@ -129,4 +203,30 @@ class HomeActivity : BaseActivity( binding.tvHomeSightChance.text = spannableBuilder } + + private fun navigateToProfileEditActivity() { + val currentState = viewModel.profile.value + if (currentState is UiState.Success) { + editProfileLauncher.launch(Intent(this, ProfileEditActivity::class.java).apply { + with(currentState.data) { + putExtra(PROFILE_NAME, this.nickname) + putExtra(PROFILE_IMAGE, this.profileImage) + putExtra(PROFILE_CHEER_TEAM, this.teamId) + } + }) + } + + } + + private fun navigateToSeatRecordActivity() { + Intent(this, SeatRecordActivity::class.java).apply { startActivity(this) } + } + + private fun navigateToReviewActivity() { + Intent(this, ReviewActivity::class.java).apply { startActivity(this) } + } + + private fun navigateToStadiumActivity() { + Intent(this, StadiumActivity::class.java).apply { startActivity(this) } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/home/ProfileEditActivity.kt b/presentation/src/main/java/com/depromeet/presentation/home/ProfileEditActivity.kt index daebf722..c284f7b5 100644 --- a/presentation/src/main/java/com/depromeet/presentation/home/ProfileEditActivity.kt +++ b/presentation/src/main/java/com/depromeet/presentation/home/ProfileEditActivity.kt @@ -1,23 +1,32 @@ package com.depromeet.presentation.home +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.text.Editable -import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import androidx.activity.viewModels import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.Lifecycle import androidx.lifecycle.asLiveData +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import coil.load import coil.transform.CircleCropTransformation import com.depromeet.core.base.BaseActivity +import com.depromeet.core.state.UiState +import com.depromeet.domain.entity.response.home.BaseballTeamResponse import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ActivityProfileEditBinding -import com.depromeet.presentation.extension.NickNameError +import com.depromeet.presentation.extension.toast +import com.depromeet.presentation.home.adapter.BaseballTeamAdapter import com.depromeet.presentation.home.adapter.GridSpacingItemDecoration -import com.depromeet.presentation.home.adapter.ProfileEditTeamAdapter -import com.depromeet.presentation.home.mockdata.TeamData -import com.depromeet.presentation.home.viewmodel.EditUiState import com.depromeet.presentation.home.viewmodel.ProfileEditViewModel +import com.depromeet.presentation.home.viewmodel.ProfileEvents +import com.depromeet.presentation.login.viewmodel.NicknameInputState import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch @AndroidEntryPoint class ProfileEditActivity : BaseActivity( @@ -26,40 +35,62 @@ class ProfileEditActivity : BaseActivity( companion object { private const val GRID_SPAN_COUNT = 2 private const val GRID_SPACING = 40 + const val PROFILE_NAME = "profile_name" + const val PROFILE_IMAGE = "profile_image" + const val PROFILE_CHEER_TEAM = "profile_cheer_team" + const val PROFILE_CHEER_TEAM_URL = "profile_cheer_team_url" } - private lateinit var adapter: ProfileEditTeamAdapter + private lateinit var adapter: BaseballTeamAdapter private val viewModel: ProfileEditViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setCheerTeamList() - navigateToPhotoPicker() - onClickTeam() - observeNickName() - viewModel.getInformation() - viewModel.uiState.asLiveData().observe(this) { state -> - updateUI(state) + viewModel.getBaseballTeam() + initView() + initEvent() + initObserver() + } + + private fun initView() { + getDataExtra { name, image, cheerTeam -> + viewModel.initProfile(name, image, cheerTeam) + binding.etProfileEditNickname.setText(name) } + setCheerTeamList() + } + private fun initEvent() { binding.ibProfileEditClose.setOnClickListener { finish() } - binding.tvProfileEditComplete.setOnClickListener { finish() } + binding.tvProfileEditComplete.setOnClickListener { + viewModel.uploadProfileEdit() + } + navigateToPhotoPicker() + onClickTeam() + } + private fun initObserver() { + observeEvent() + observeNickName() + observeTeam() + observeProfileImage() + observeChange() } - private fun updateUI(state: EditUiState) { - binding.ivProfileEditImage.load(state.image) { - transformations(CircleCropTransformation()) - placeholder(R.drawable.ic_default_profile) - error(R.drawable.ic_default_profile) + private fun saveProfile() { + val resultIntent = Intent().apply { + putExtra(PROFILE_NAME, viewModel.nickname.value) + putExtra(PROFILE_IMAGE, viewModel.getPresignedUrlOrProfileImage()) + putExtra(PROFILE_CHEER_TEAM, viewModel.cheerTeam.value) + putExtra(PROFILE_CHEER_TEAM_URL, viewModel.getTeamUrl()) } - binding.etProfileEditNickname.setText(state.nickName) - adapter.submitList(state.teamList) + setResult(Activity.RESULT_OK, resultIntent) + finish() } private fun setCheerTeamList() { - adapter = ProfileEditTeamAdapter() + adapter = BaseballTeamAdapter() binding.rvProfileEditTeam.adapter = adapter binding.rvProfileEditTeam.addItemDecoration( GridSpacingItemDecoration( @@ -69,35 +100,52 @@ class ProfileEditActivity : BaseActivity( ) } + private fun observeEvent() { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.events.collect { event -> + when (event) { + is ProfileEvents.ShowSnackMessage -> { + toast(event.message) + } + } + } + } + + } + } + private fun observeNickName() { binding.etProfileEditNickname.addTextChangedListener { text: Editable? -> viewModel.updateNickName(text.toString()) } - viewModel.nickNameError.asLiveData().observe(this) { error -> - when (error) { - is NickNameError.NoError -> { + viewModel.nickNameError.asLiveData().observe(this) { state -> + when (state) { + NicknameInputState.EMPTY -> {} + + NicknameInputState.VALID -> { updateCompletionStatus( isError = false, getString(R.string.profile_edit_error_no) ) } - is NickNameError.LengthError -> { + NicknameInputState.INVALID_LENGTH -> { updateCompletionStatus( isError = true, getString(R.string.profile_edit_error_length) ) } - is NickNameError.InvalidCharacterError -> { + NicknameInputState.INVALID_CHARACTER -> { updateCompletionStatus( isError = true, getString(R.string.profile_edit_error_type) ) } - is NickNameError.DuplicateError -> { + NicknameInputState.DUPLICATE -> { updateCompletionStatus( isError = true, getString(R.string.profile_edit_error_duplicate) @@ -107,27 +155,101 @@ class ProfileEditActivity : BaseActivity( } } + private fun observeProfileImage() { + viewModel.profileImage.asLiveData().observe(this) { state -> + with(binding.ivProfileEditImage) { + if (state.isEmpty()) { + setImageResource(R.drawable.ic_default_profile) + } else { + load(state) { + transformations(CircleCropTransformation()) + } + } + } + } + + viewModel.presignedUrl.asLiveData().observe(this) { state -> + when (state) { + is UiState.Loading -> {} + is UiState.Success -> {} + is UiState.Failure -> { + toast("이미지 업로드를 실패하였습니다. 다시 선택해주세요.") + } + + is UiState.Empty -> { + toast("실패") + } + } + } + } + + private fun observeChange() { + viewModel.changeEnabled.asLiveData().observe(this) { state -> + binding.tvProfileEditComplete.isEnabled = state + + } + + viewModel.profileEdit.asLiveData().observe(this) { state -> + when (state) { + is UiState.Success -> { + saveProfile() + } + + is UiState.Failure -> { + toast("프로필 변경에 실패\n다시 시도바람") + } + + is UiState.Empty -> { + toast("프로필 변경 실패\n다시 시도바람(빈값") + } + + is UiState.Loading -> {} + } + + } + } + + private fun observeTeam() { + viewModel.team.asLiveData().observe(this) { state -> + when (state) { + is UiState.Success -> { + adapter.submitList(state.data) + } + + is UiState.Loading -> { + toast("로딩 중") + } + + is UiState.Empty -> { + toast("빈값 에러") + } + + is UiState.Failure -> { + toast("통신 실패") + } + } + } + } + private fun updateCompletionStatus(isError: Boolean, error: String) = if (isError) { with(binding) { etProfileEditNickname.setBackgroundResource(R.drawable.rect_warning01red_line_6) - tvProfileEditNicknameError.visibility = View.VISIBLE + tvProfileEditNicknameError.visibility = VISIBLE tvProfileEditNicknameError.text = error - tvProfileEditComplete.isEnabled = false } } else { with(binding) { etProfileEditNickname.setBackgroundResource(R.drawable.rect_gray100_line_6) - tvProfileEditNicknameError.visibility = View.GONE + tvProfileEditNicknameError.visibility = GONE tvProfileEditNicknameError.text = error - tvProfileEditComplete.isEnabled = true } } private fun onClickTeam() { - adapter.itemClubClickListener = object : ProfileEditTeamAdapter.OnItemClubClickListener { - override fun onItemClubClick(item: TeamData) { - viewModel.updateClickTeam(item) + adapter.itemClubClickListener = object : BaseballTeamAdapter.OnItemClubClickListener { + override fun onItemClubClick(item: BaseballTeamResponse) { + viewModel.setClickedBaseballTeam(item.id) binding.tvProfileEditNoTeam.setBackgroundResource(R.drawable.rect_gray100_line_10) } } @@ -147,4 +269,12 @@ class ProfileEditActivity : BaseActivity( fragment.show(supportFragmentManager, fragment.tag) } + private fun getDataExtra(callback: (name: String, profileImage: String, cheerTeam: Int) -> Unit) { + callback( + intent?.getStringExtra(HomeActivity.PROFILE_NAME) ?: "", + intent?.getStringExtra(HomeActivity.PROFILE_IMAGE) ?: "", + intent?.getIntExtra(HomeActivity.PROFILE_CHEER_TEAM, 0) ?: 0 + ) + } + } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/home/ProfileImageUploadDialog.kt b/presentation/src/main/java/com/depromeet/presentation/home/ProfileImageUploadDialog.kt index a08137dc..bb5d4a74 100644 --- a/presentation/src/main/java/com/depromeet/presentation/home/ProfileImageUploadDialog.kt +++ b/presentation/src/main/java/com/depromeet/presentation/home/ProfileImageUploadDialog.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.net.Uri import android.os.Bundle import android.view.View +import android.webkit.MimeTypeMap import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.activityViewModels @@ -39,7 +40,8 @@ class ProfileImageUploadDialog() : BindingBottomSheetDialog( +class BaseballTeamAdapter : ListAdapter( ItemDiffCallback( onItemsTheSame = { oldItem, newItem -> oldItem.name == newItem.name }, onContentsTheSame = { oldItem, newItem -> oldItem == newItem } ) ) { interface OnItemClubClickListener { - fun onItemClubClick(item: TeamData) + fun onItemClubClick(item: BaseballTeamResponse) } var itemClubClickListener: OnItemClubClickListener? = null @@ -46,9 +46,9 @@ class ProfileEditTeamAdapter : ListAdapter( class ProfileEditTeamViewHolder( private val binding: ItemBaseballTeamBinding, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: TeamData) { + fun bind(item: BaseballTeamResponse) { with(binding) { - ivTeamImage.load(item.image) { + ivTeamImage.load(item.logo) { placeholder(R.drawable.ic_lg_team) error(R.drawable.ic_x_close) } diff --git a/presentation/src/main/java/com/depromeet/presentation/home/mockdata/HomeMockData.kt b/presentation/src/main/java/com/depromeet/presentation/home/mockdata/HomeMockData.kt deleted file mode 100644 index 813345ab..00000000 --- a/presentation/src/main/java/com/depromeet/presentation/home/mockdata/HomeMockData.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.depromeet.presentation.home.mockdata - -import android.os.Parcelable -import com.depromeet.presentation.home.viewmodel.HomeUiState -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.parcelize.Parcelize -import kotlin.random.Random - -/** - * 테스트용 - */ -@Parcelize -data class Profile( - val nickName: String = "", - val writeCount: Int = 0, - val level: Int = 0, - val title: String = "", - val cheerTeam: String = "", -) : Parcelable - -@Parcelize -data class RecentSight( - val location: String = "", - val date: String = "", - val section: String = "", - val photoList: List = emptyList(), -) : Parcelable - -data class LevelPolicy( - val name: String, - val minCount: Int, - val maxCount: Int, - val level: Int, -) - -/*** - * 화면 테스트용으로 랜덤하게 flow return - */ -fun mockDataProfile(): Flow = flow { - - /*** - * 추후 클라에서 처리할수도 있을지도..? 아닌가 - */ - val levelPolicies = listOf( - LevelPolicy("직관 첫 걸음", 0, 2, 1), - LevelPolicy("경기장 탐험가", 3, 6, 2), - LevelPolicy("직관의 여유", 7, 11, 3), - LevelPolicy("응원 단장", 12, 20, 4), - LevelPolicy("야구장 VIP", 21, 35, 5), - LevelPolicy("전설의 직관러", 36, Int.MAX_VALUE, 6) - ) - - fun getLevelByCount(count: Int): Pair { - val levelPolicy = levelPolicies.first { count in it.minCount..it.maxCount } - return Pair(levelPolicy.level, levelPolicy.name) - } - - - val profileList = listOf( - Profile("노균욱", 23, 5, "야구장 VIP", "https://picsum.photos/600/400"), - Profile("윤성식", 12, 4, "응원 단장", "https://picsum.photos/600/400"), - Profile("박민주", 2, 2, "직관 첫 걸음", "https://picsum.photos/600/400"), - Profile("조관희", 7, 3, "직관의 여유", "https://picsum.photos/600/400"), - ) - val recentSightList = listOf( - RecentSight("서울 잠실 야구장", "2024년 7월 20일", "3루 네이비석 3054블럭", emptyList()), - RecentSight( - "대전 한화 야구장", - "2024년 7월 12일", - "1루 블루석 124블럭", - listOf("https://picsum.photos/600/400") - ), - RecentSight( - "고척 스카이돔", "2024년 7월 05일", "3루 레드석 234블럭", listOf( - "https://picsum.photos/600/400", - "https://picsum.photos/600/400" - ) - ), - RecentSight( - "대구 삼성 파크", "2024년 6월 23일", "3루 베이지석 3451블럭", listOf( - "https://picsum.photos/600/400", - "https://picsum.photos/600/400", - "https://picsum.photos/600/400" - ) - ) - ) - delay(300) - val randomNumber = Random.nextInt(0, 4) - emit( - HomeUiState( - profileList[randomNumber], - recentSightList[randomNumber] - ) - ) -} - - diff --git a/presentation/src/main/java/com/depromeet/presentation/home/mockdata/ProfileEditMockData.kt b/presentation/src/main/java/com/depromeet/presentation/home/mockdata/ProfileEditMockData.kt index f8726880..2ef8b8fa 100644 --- a/presentation/src/main/java/com/depromeet/presentation/home/mockdata/ProfileEditMockData.kt +++ b/presentation/src/main/java/com/depromeet/presentation/home/mockdata/ProfileEditMockData.kt @@ -2,7 +2,6 @@ package com.depromeet.presentation.home.mockdata import android.os.Parcelable import com.depromeet.presentation.R -import com.depromeet.presentation.home.viewmodel.EditUiState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.parcelize.Parcelize @@ -26,4 +25,10 @@ fun mockDataProfileEdit(): Flow = flow { } emit(EditUiState("https://picsum.photos/600/400", "노균욱", testData)) -} \ No newline at end of file +} + +data class EditUiState( + val image: String = "", + val nickName: String = "", + val teamList: List = emptyList(), +) \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/HomeViewModel.kt b/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/HomeViewModel.kt index c5cd001a..e5ca5ab9 100644 --- a/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/HomeViewModel.kt +++ b/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/HomeViewModel.kt @@ -1,36 +1,69 @@ package com.depromeet.presentation.home.viewmodel -import android.os.Parcelable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.depromeet.core.state.UiState -import com.depromeet.presentation.home.mockdata.Profile -import com.depromeet.presentation.home.mockdata.RecentSight -import com.depromeet.presentation.home.mockdata.mockDataProfile +import com.depromeet.domain.entity.response.home.ProfileResponse +import com.depromeet.domain.entity.response.home.RecentReviewResponse +import com.depromeet.domain.repository.HomeRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.Parcelize +import kotlinx.coroutines.launch import javax.inject.Inject -@Parcelize -data class HomeUiState( - val profile: Profile = Profile(), - val recentSight: RecentSight = RecentSight(), -) : Parcelable @HiltViewModel -class HomeViewModel @Inject constructor() : ViewModel() { +class HomeViewModel @Inject constructor( + private val homeRepository: HomeRepository, +) : ViewModel() { - private val _uiState = MutableStateFlow>(UiState.Loading) - val uiState: StateFlow> = _uiState.asStateFlow() + + private val _profile = MutableStateFlow>(UiState.Loading) + val profile = _profile.asStateFlow() + + private val _recentReview = MutableStateFlow>(UiState.Loading) + val recentReview = _recentReview.asStateFlow() + + val nickname = MutableStateFlow("") + val reviewCount = MutableStateFlow(0) fun getInformation() { - mockDataProfile().onEach { - _uiState.value = UiState.Success(it) - }.launchIn(viewModelScope) + viewModelScope.launch { + val profileDeferred = async { + homeRepository.getProfile() + } + val reviewDeferred = async { + homeRepository.getRecentReview() + } + + val profileResult = profileDeferred.await() + profileResult.onSuccess { + _profile.value = UiState.Success(it) + nickname.value = it.nickname + }.onFailure { + _profile.value = UiState.Failure(it.message ?: "실패") + } + + val reviewResult = reviewDeferred.await() + reviewResult.onSuccess { + _recentReview.value = UiState.Success(it) + reviewCount.value = it.reviewCount + }.onFailure { + _recentReview.value = UiState.Failure(it.message ?: "실패") + } + } + } + + fun updateTest(name: String, image: String, cheerTeam: Int, cheerTeamUrl: String) { + _profile.value = UiState.Success( + (_profile.value as UiState.Success).data.copy( + nickname = name, + profileImage = image, + teamId = cheerTeam, + teamImage = cheerTeamUrl + ) + ) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/ProfileEditViewModel.kt b/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/ProfileEditViewModel.kt index 67e24df8..a29555b5 100644 --- a/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/ProfileEditViewModel.kt +++ b/presentation/src/main/java/com/depromeet/presentation/home/viewmodel/ProfileEditViewModel.kt @@ -1,77 +1,243 @@ package com.depromeet.presentation.home.viewmodel +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.depromeet.presentation.extension.NickNameError +import com.depromeet.core.state.UiState +import com.depromeet.domain.entity.request.home.ProfileEditRequest +import com.depromeet.domain.entity.response.home.BaseballTeamResponse +import com.depromeet.domain.entity.response.home.PresignedUrlResponse +import com.depromeet.domain.entity.response.home.ProfileEditResponse +import com.depromeet.domain.repository.HomeRepository import com.depromeet.presentation.extension.validateNickName -import com.depromeet.presentation.home.mockdata.TeamData -import com.depromeet.presentation.home.mockdata.mockDataProfileEdit +import com.depromeet.presentation.login.viewmodel.NicknameInputState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch import javax.inject.Inject -data class EditUiState( - val image: String = "", - val nickName: String = "", - val teamList: List = emptyList(), -) - +sealed class ProfileEvents { + data class ShowSnackMessage( + val message: String, + ) : ProfileEvents() +} @HiltViewModel -class ProfileEditViewModel @Inject constructor() : ViewModel() { +class ProfileEditViewModel @Inject constructor( + private val homeRepository: HomeRepository, +) : ViewModel() { + + private val _events = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val events: SharedFlow = _events.asSharedFlow() + + private val _team = MutableStateFlow>>(UiState.Loading) + val team = _team.asStateFlow() - private val _uiState = MutableStateFlow(EditUiState()) - val uiState: StateFlow = _uiState.asStateFlow() + private val _presignedUrl = MutableStateFlow>(UiState.Loading) + val presignedUrl = _presignedUrl.asStateFlow() - private val _nickName = MutableStateFlow("") - val nickName = _nickName.asStateFlow() + private val _profileEdit = MutableStateFlow>(UiState.Loading) + val profileEdit = _profileEdit.asStateFlow() - private val _nickNameError = MutableStateFlow(NickNameError.NoError) + private val _nickname = MutableStateFlow("") + val nickname = _nickname.asStateFlow() + + private val _nickNameError = MutableStateFlow(NicknameInputState.VALID) val nickNameError = _nickNameError.asStateFlow() - fun getInformation() { - mockDataProfileEdit().onEach { - _uiState.value = it - }.launchIn(viewModelScope) + private val _profileImage = MutableStateFlow("") + val profileImage = _profileImage.asStateFlow() + + private val _cheerTeam = MutableStateFlow(0) + val cheerTeam = _cheerTeam.asStateFlow() + + val changeEnabled = MutableStateFlow(false) + private var profileImageJob: Job? = null + private var profilePresignedJob: Job? = null + + private var initialNickName: String = "" + private var initialProfileImage: String = "" + private var initialCheerTeam: Int = 0 + + init { + viewModelScope.launch { + combine( + nickname, + profileImage, + cheerTeam, + nickNameError + ) { nickName, profileImage, cheerTeam, nickNameError -> + (nickName != initialNickName || profileImage != initialProfileImage + || cheerTeam != initialCheerTeam) && nickNameError == NicknameInputState.VALID + }.collect { isChanged -> + changeEnabled.value = isChanged + } + } + } + + fun getBaseballTeam() { + viewModelScope.launch { + homeRepository.getBaseballTeam() + .onSuccess { teams -> + val updatedTeams = teams.map { team -> + team.copy(isClicked = team.id == cheerTeam.value) + } + _team.value = UiState.Success(updatedTeams) + }.onFailure { + _team.value = UiState.Failure(it.message ?: "실패") + } + } + } + + fun deleteProfileImage() { + _profileImage.value = "" } fun setProfileImage(uri: String) { - val newState = _uiState.value.copy(image = uri) - _uiState.value = newState + _profileImage.value = uri } - fun updateClickTeam(team: TeamData) { - val currentTeamList = uiState.value.teamList.toMutableList() - val newCheckedTeamIndex = currentTeamList.indexOfFirst { it.id == team.id }.apply { - if (this == -1) return + fun setProfileImagePresigned( + profileByteArray: ByteArray, + fileExtension: String, + ) { + profilePresignedJob = viewModelScope.launch { + homeRepository.postProfileImagePresigned(fileExtension) + .onSuccess { presignedUrl -> + _presignedUrl.value = UiState.Success(presignedUrl) + uploadProfileImage(profileByteArray) + } + .onFailure { + _presignedUrl.value = UiState.Failure(it.message ?: "실패") + } } - val beforeCheckedTeamIndex = currentTeamList.indexOfFirst { it.isClicked } + } - if (newCheckedTeamIndex == beforeCheckedTeamIndex) { - return + private fun uploadProfileImage(profileByteArray: ByteArray) { + val currentState = presignedUrl.value + if (currentState is UiState.Success) { + profileImageJob = viewModelScope.launch { + homeRepository.putProfileImage( + currentState.data.presignedUrl, + profileByteArray + ) + .onSuccess {} + .onFailure { + _events.emit(ProfileEvents.ShowSnackMessage("프로필 이미지 업로드에 실패하였습니다\n다시 시도해주세요~")) + setProfileImage(initialProfileImage) + } + } } + } + + fun uploadProfileEdit() { + viewModelScope.launch { + if ((profileImageJob?.isActive == true) || (profilePresignedJob?.isActive == true)) { + profileImageJob?.join() + profilePresignedJob?.join() + } - currentTeamList[newCheckedTeamIndex] = - currentTeamList[newCheckedTeamIndex].copy(isClicked = true) - if (beforeCheckedTeamIndex != -1) { - currentTeamList[beforeCheckedTeamIndex] = - currentTeamList[beforeCheckedTeamIndex].copy(isClicked = false) + homeRepository.putProfileEdit( + ProfileEditRequest( + url = getPresignedUrlOrProfileImage(), + nickname = nickname.value, + teamId = cheerTeam.value + ) + ) + .onSuccess { + _profileEdit.value = UiState.Success(it) + } + .onFailure { + _events.emit(ProfileEvents.ShowSnackMessage("프로필 업데이트에 실패하였습니다 다시 시도해주세요")) + } } + } - _uiState.value = _uiState.value.copy(teamList = currentTeamList) + fun getPresignedUrlOrProfileImage(): String { + val currentState = presignedUrl.value + return if (currentState is UiState.Success) { + removeQueryParameters(currentState.data.presignedUrl) + } else { + profileImage.value + } + } + + fun getTeamUrl() : String { + if (cheerTeam.value == 0) return "" + return (team.value as UiState.Success).data.first { it.isClicked }.logo + } + + private fun removeQueryParameters(url: String): String { + val uri = Uri.parse(url) + return uri.buildUpon().clearQuery().build().toString() + } + + + fun setClickedBaseballTeam(id: Int) { + val currentState = team.value + if (currentState is UiState.Success) { + val updatedList = currentState.data.map { team -> + team.copy(isClicked = team.id == id) + } + _team.value = UiState.Success(updatedList) + _cheerTeam.value = id + } } fun deleteCheerTeam() { - val currentTeamList = uiState.value.teamList.map { it.copy(isClicked = false) } - _uiState.value = _uiState.value.copy(teamList = currentTeamList) + val currentState = team.value + if (currentState is UiState.Success) { + val updatedList = currentState.data.map { team -> + team.copy(isClicked = false) + } + _team.value = UiState.Success(updatedList) + _cheerTeam.value = 0 + } + } + + fun initProfile(name: String, image: String, cheerTeam: Int) { + initialNickName = name + initialProfileImage = image + initialCheerTeam = cheerTeam + + _nickname.value = name + _profileImage.value = image + _cheerTeam.value = cheerTeam + + changeEnabled.value = false } fun updateNickName(nickName: String) { - _nickName.value = nickName + _nickname.value = nickName _nickNameError.value = nickName.validateNickName() + + if (_nickNameError.value == NicknameInputState.VALID && initialNickName != nickname.value) { + checkNickNameAvailability(nickName) + } } + + private fun checkNickNameAvailability(nickName: String) { + viewModelScope.launch { + homeRepository.getDuplicateNickname(nickName) + .onSuccess { + _nickNameError.value = NicknameInputState.VALID + } + .onFailure { + _nickNameError.value = NicknameInputState.DUPLICATE + } + } + } + + } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/login/TeamSelectAdapter.kt b/presentation/src/main/java/com/depromeet/presentation/login/TeamSelectAdapter.kt index ca31e31d..7e30bab1 100644 --- a/presentation/src/main/java/com/depromeet/presentation/login/TeamSelectAdapter.kt +++ b/presentation/src/main/java/com/depromeet/presentation/login/TeamSelectAdapter.kt @@ -4,10 +4,10 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import coil.load import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ItemBaseballTeamBinding import com.depromeet.presentation.databinding.ItemSelectTeamBinding -import com.depromeet.presentation.home.adapter.ProfileEditTeamViewHolder import com.depromeet.presentation.home.mockdata.TeamData import com.depromeet.presentation.util.ItemDiffCallback @@ -81,4 +81,27 @@ class ButtonViewHolder( nextClick() } } +} + +class ProfileEditTeamViewHolder( + private val binding: ItemBaseballTeamBinding, +) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: TeamData) { + with(binding) { + ivTeamImage.load(item.image) { + placeholder(R.drawable.ic_lg_team) + error(R.drawable.ic_x_close) + } + tvTeamName.text = item.name + updateSelectState(item.isClicked) + } + } + + private fun updateSelectState(isSelected: Boolean) { + val backgroundRes = when (isSelected) { + true -> R.drawable.rect_gray50_fill_gray900_line_10 + false -> R.drawable.rect_gray100_line_10 + } + binding.root.setBackgroundResource(backgroundRes) + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/login/ui/NicknameInputFragment.kt b/presentation/src/main/java/com/depromeet/presentation/login/ui/NicknameInputFragment.kt index a070a799..10f477f2 100644 --- a/presentation/src/main/java/com/depromeet/presentation/login/ui/NicknameInputFragment.kt +++ b/presentation/src/main/java/com/depromeet/presentation/login/ui/NicknameInputFragment.kt @@ -93,6 +93,11 @@ class NicknameInputFragment: BindingFragment( tvNicknameWarning.text = getString(R.string.profile_edit_error_type) updateButtonEnabled(false) } + NicknameInputState.DUPLICATE -> { + clNicknameInputWarning.visibility = View.VISIBLE + tvNicknameWarning.text = getString(R.string.profile_edit_error_duplicate) + updateButtonEnabled(false) + } } } } diff --git a/presentation/src/main/java/com/depromeet/presentation/login/viewmodel/SignUpViewModel.kt b/presentation/src/main/java/com/depromeet/presentation/login/viewmodel/SignUpViewModel.kt index d6632f8a..193c7bb8 100644 --- a/presentation/src/main/java/com/depromeet/presentation/login/viewmodel/SignUpViewModel.kt +++ b/presentation/src/main/java/com/depromeet/presentation/login/viewmodel/SignUpViewModel.kt @@ -102,5 +102,6 @@ enum class NicknameInputState { EMPTY, VALID, INVALID_LENGTH, - INVALID_CHARACTER + INVALID_CHARACTER, + DUPLICATE } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/ConfirmDeleteDialog.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/ConfirmDeleteDialog.kt index 2f7aedf6..4190d184 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/ConfirmDeleteDialog.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/ConfirmDeleteDialog.kt @@ -1,5 +1,6 @@ package com.depromeet.presentation.seatrecord +import android.content.DialogInterface import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle @@ -9,7 +10,7 @@ import androidx.fragment.app.activityViewModels import com.depromeet.core.base.BindingDialogFragment import com.depromeet.presentation.R import com.depromeet.presentation.databinding.FragmentConfirmDeleteDialogBinding -import com.depromeet.presentation.seatrecord.viewmodel.SeatDetailViewModel +import com.depromeet.presentation.seatrecord.viewmodel.SeatRecordViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -17,11 +18,26 @@ class ConfirmDeleteDialog : BindingDialogFragment( - ActivitySeatDetailRecordBinding::inflate -) { - private lateinit var detailRecordAdapter: DetailRecordAdapter - private val viewModel: SeatDetailViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setDetailRecordAdapter() - viewModel.getReviewData() - viewModel.uiState.asLiveData().observe(this) { - detailRecordAdapter.submitList(it.list) - } - - viewModel.deleteClickedEvent.asLiveData().observe(this) { state -> - if (state) moveConfirmationDialog() - } - - } - - private fun setDetailRecordAdapter() { - detailRecordAdapter = DetailRecordAdapter() - binding.rvDetailRecord.adapter = detailRecordAdapter - - detailRecordAdapter.itemMoreClickListener = - object : DetailRecordAdapter.OnDetailItemClickListener { - override fun onItemMoreClickListener(item: ReviewDetailMockData) { - viewModel.setEditReviewId(item.reviewId) - RecordEditDialog().apply { show(supportFragmentManager, this.tag) } - } - } - } - - private fun moveConfirmationDialog() { - ConfirmDeleteDialog().apply { show(supportFragmentManager, this.tag) } - } - -} \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/SeatDetailRecordFragment.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/SeatDetailRecordFragment.kt new file mode 100644 index 00000000..b55e8e7c --- /dev/null +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/SeatDetailRecordFragment.kt @@ -0,0 +1,97 @@ +package com.depromeet.presentation.seatrecord + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.asLiveData +import com.depromeet.core.base.BindingFragment +import com.depromeet.core.state.UiState +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import com.depromeet.presentation.R +import com.depromeet.presentation.databinding.ActivitySeatDetailRecordBinding +import com.depromeet.presentation.seatReview.ReviewActivity +import com.depromeet.presentation.seatrecord.adapter.TestDetailRecordAdapter +import com.depromeet.presentation.seatrecord.viewmodel.DeleteUi +import com.depromeet.presentation.seatrecord.viewmodel.SeatRecordViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SeatDetailRecordFragment : BindingFragment( + layoutResId = R.layout.activity_seat_detail_record, + bindingInflater = ActivitySeatDetailRecordBinding::inflate +) { + companion object { + const val SEAT_RECORD_TAG = "seatRecord" + } + + private val viewModel: SeatRecordViewModel by activityViewModels() + private lateinit var testDetailRecordAdapter: TestDetailRecordAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initView() + initEvent() + initObserver() + + } + + private fun initView() { + setDetailRecordAdapter() + } + + private fun initEvent() { + with(binding) { + fabDetailUp.setOnClickListener { + rvDetailRecord.smoothScrollToPosition(0) + } + detailRecordAppbar.setNavigationOnClickListener { + parentFragmentManager.popBackStack() + } + fabDetailPlus.setOnClickListener { + Intent( + requireActivity(), + ReviewActivity::class.java + ).apply { startActivity(this) } + } + detailRecordAppbar.setMenuOnClickListener { + /** 셋팅 이동 **/ + } + } + } + + private fun initObserver() { + viewModel.reviews.asLiveData().observe(viewLifecycleOwner) { state -> + if (state is UiState.Success) { + testDetailRecordAdapter.submitList(state.data.reviews) + } + } + + viewModel.deleteClickedEvent.asLiveData().observe(viewLifecycleOwner) { state -> + if (state == DeleteUi.SEAT_DETAIL) moveConfirmationDialog() + } + } + + private fun setDetailRecordAdapter() { + testDetailRecordAdapter = TestDetailRecordAdapter( + (viewModel.reviews.value as UiState.Success).data.profile + ) + + binding.rvDetailRecord.adapter = testDetailRecordAdapter + + testDetailRecordAdapter.itemMoreClickListener = + object : TestDetailRecordAdapter.OnDetailItemClickListener { + override fun onItemMoreClickListener(item: MySeatRecordResponse.ReviewResponse) { + viewModel.setEditReviewId(item.id) + RecordEditDialog.newInstance(SEAT_RECORD_TAG) + .show(parentFragmentManager, RecordEditDialog.TAG) + } + } + } + + private fun moveConfirmationDialog() { + ConfirmDeleteDialog.newInstance(SEAT_RECORD_TAG) + .show(parentFragmentManager, ConfirmDeleteDialog.TAG) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/SeatRecordActivity.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/SeatRecordActivity.kt index 200d9ebf..780d8277 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/SeatRecordActivity.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/SeatRecordActivity.kt @@ -3,32 +3,40 @@ package com.depromeet.presentation.seatrecord import android.content.Intent import android.os.Bundle import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.widget.AdapterView import androidx.activity.viewModels +import androidx.fragment.app.commit import androidx.lifecycle.asLiveData import coil.load import coil.transform.CircleCropTransformation import com.depromeet.core.base.BaseActivity +import com.depromeet.core.state.UiState import com.depromeet.designsystem.SpotSpinner import com.depromeet.designsystem.extension.dpToPx +import com.depromeet.domain.entity.response.home.MySeatRecordResponse import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ActivitySeatRecordBinding +import com.depromeet.presentation.extension.toast +import com.depromeet.presentation.seatReview.ReviewActivity import com.depromeet.presentation.seatrecord.adapter.DateMonthAdapter import com.depromeet.presentation.seatrecord.adapter.LinearSpacingItemDecoration import com.depromeet.presentation.seatrecord.adapter.MonthRecordAdapter -import com.depromeet.presentation.seatrecord.mockdata.MonthData -import com.depromeet.presentation.seatrecord.mockdata.ProfileDetailData -import com.depromeet.presentation.seatrecord.mockdata.ReviewMockData -import com.depromeet.presentation.seatrecord.mockdata.groupByMonth -import com.depromeet.presentation.seatrecord.mockdata.monthList +import com.depromeet.presentation.seatrecord.uiMapper.MonthReviewData +import com.depromeet.presentation.seatrecord.uiMapper.MonthUiData +import com.depromeet.presentation.seatrecord.viewmodel.DeleteUi import com.depromeet.presentation.seatrecord.viewmodel.SeatRecordViewModel +import com.depromeet.presentation.util.CalendarUtil import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber @AndroidEntryPoint class SeatRecordActivity : BaseActivity( ActivitySeatRecordBinding::inflate ) { companion object { + const val SEAT_RECORD_TAG = "seatRecord" private const val START_SPACING_DP = 20 private const val BETWEEN_SPACING_DP = 8 } @@ -40,26 +48,17 @@ class SeatRecordActivity : BaseActivity( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initDateSpinner() - initMonthAdapter() - - viewModel.getSeatRecords() - - viewModel.uiState.asLiveData().observe(this) { - setProfile(it.profileDetailData) - initReviewExist(it.reviews) - } - viewModel.selectedMonth.asLiveData().observe(this) { - val updatedMonthList = monthList.map { monthData -> - monthData.copy(isClicked = monthData.month == it) - } - dateMonthAdapter.submitList(updatedMonthList) - } + initView() + initEvent() + initObserver() + } - setClickListener() + private fun initView() { + initMonthAdapter() + viewModel.getReviewDate() } - private fun setClickListener() { + private fun initEvent() { with(binding) { recordSpotAppbar.setNavigationOnClickListener { finish() @@ -70,30 +69,101 @@ class SeatRecordActivity : BaseActivity( fabRecordUp.setOnClickListener { ssvRecord.smoothScrollTo(0, 0) } + fabRecordPlus.setOnClickListener { + Intent(this@SeatRecordActivity, ReviewActivity::class.java).apply { + startActivity( + this + ) + } + } + } + } + + private fun initObserver() { + observeDates() + observeReviews() + observeEvents() + } + + private fun observeDates() { + viewModel.selectedYear.asLiveData().observe(this) { + viewModel.initMonths() + } + viewModel.months.asLiveData().observe(this) { + dateMonthAdapter.submitList(it) + viewModel.getSeatRecords() + } + viewModel.date.asLiveData().observe(this) { state -> + when (state) { + is UiState.Success -> { + setReviewsVisibility(isExist = true) + val year = state.data.yearMonths.map { it.year } + initYearSpinner(year) + } + + is UiState.Empty -> { + setReviewsVisibility(isExist = false) + } + + is UiState.Loading -> {} + is UiState.Failure -> { + setReviewsVisibility(isExist = false) + } + + } } } - private fun setProfile(profileData: ProfileDetailData) { + private fun observeReviews() { + viewModel.reviews.asLiveData().observe(this) { state -> + when (state) { + is UiState.Success -> { + setProfile(state.data.profile) + setReviewList(state.data.reviews) + } + + is UiState.Loading -> { + toast("로딩중") + } + + is UiState.Empty -> { + toast("오류 발생 빈값") + } + + is UiState.Failure -> { + Timber.d("test : ${state.msg}") + } + } + } + } + + private fun observeEvents() { + viewModel.deleteClickedEvent.asLiveData().observe(this) { state -> + if (state == DeleteUi.SEAT_RECORD) moveConfirmationDialog() + } + } + + private fun setProfile(data: MySeatRecordResponse.MyProfileResponse) { with(binding) { - "Lv.${profileData.level} ${profileData.titleName}".also { tvRecordLevel.text = it } - tvRecordNickname.text = profileData.nickName - tvRecordCount.text = profileData.recordCount.toString() - ivRecordProfile.load(profileData.profileImage) { + "Lv.${data.level} ${data.levelTitle}".also { tvRecordLevel.text = it } + tvRecordNickname.text = data.nickname + tvRecordCount.text = data.reviewCount.toString() + ivRecordProfile.load(data.profileImage) { transformations(CircleCropTransformation()) + error(R.drawable.ic_default_profile) } } } - private fun initDateSpinner() { - val year = listOf("2024년", "2023년", "2022년", "2021년") -// val adapter = ArrayAdapter(this, R.layout.item_custom_month_spinner_view, year) -// adapter.setDropDownViewResource(R.layout.item_custom_month_spinner_dropdown) + private fun initYearSpinner(years: List) { + viewModel.setSelectedYear(years[0]) + val yearList = years.map { "${it}년" } val adapter = SpotSpinner( this, R.layout.item_custom_month_spinner_view, R.layout.item_custom_month_spinner_dropdown, - year, + yearList, true, R.color.gray900 ) @@ -108,8 +178,9 @@ class SeatRecordActivity : BaseActivity( id: Long, ) { adapter.setSelectedItemPosition(position) - val selectedYear = year[position].filter { it.isDigit() }.toInt() + val selectedYear = yearList[position].filter { it.isDigit() }.toInt() viewModel.setSelectedYear(selectedYear) + binding.ssvRecord.smoothScrollTo(0, 0) } override fun onNothingSelected(p0: AdapterView<*>?) {} @@ -128,45 +199,69 @@ class SeatRecordActivity : BaseActivity( ) dateMonthAdapter.itemMonthClickListener = object : DateMonthAdapter.OnItemMonthClickListener { - override fun onItemMonthClick(item: MonthData) { + override fun onItemMonthClick(item: MonthUiData) { val selectedMonth = item.month viewModel.setSelectedMonth(selectedMonth) + binding.ssvRecord.smoothScrollTo(0, 0) } } } - private fun initReviewExist(reviews: List) { - if (reviews.isEmpty()) { + private fun setReviewsVisibility(isExist: Boolean) { + if (!isExist) { with(binding) { - "${viewModel.selectedMonth.value}년".also { tvRecordYear.text = it } - clRecordNone.visibility = View.VISIBLE - clRecordStickyHeader.visibility = View.GONE - rvRecordMonthDetail.visibility = View.GONE + "${CalendarUtil.getCurrentYear()}년".also { tvRecordYear.text = it } + clRecordNone.visibility = VISIBLE + clRecordStickyHeader.visibility = GONE + rvRecordMonthDetail.visibility = GONE } } else { with(binding) { - clRecordNone.visibility = View.GONE - clRecordStickyHeader.visibility = View.VISIBLE - rvRecordMonthDetail.visibility = View.VISIBLE + clRecordNone.visibility = GONE + clRecordStickyHeader.visibility = VISIBLE + rvRecordMonthDetail.visibility = VISIBLE - monthRecordAdapter = MonthRecordAdapter() - rvRecordMonthDetail.adapter = monthRecordAdapter ssvRecord.header = binding.clRecordStickyHeader } - monthRecordAdapter.submitList(reviews.groupByMonth()) - - monthRecordAdapter.itemRecordClickListener = - object : MonthRecordAdapter.OnItemRecordClickListener { - override fun onItemRecordClick(item: ReviewMockData) { - Intent( - this@SeatRecordActivity, - SeatDetailRecordActivity::class.java - ).apply { - startActivity(this) - } + } + } + + private fun setReviewList(reviews: List) { + val groupList = + reviews.groupBy { CalendarUtil.getMonthFromDateFormat(it.date) } + .map { (month, reviews) -> + MonthReviewData(month, reviews) + } + monthRecordAdapter = MonthRecordAdapter() + binding.rvRecordMonthDetail.adapter = monthRecordAdapter + monthRecordAdapter.submitList(groupList) + + monthRecordAdapter.itemRecordClickListener = + object : MonthRecordAdapter.OnItemRecordClickListener { + + override fun onItemRecordClick(item: MySeatRecordResponse.ReviewResponse) { + + supportFragmentManager.commit { + replace( + R.id.fcv_record, + SeatDetailRecordFragment(), + SeatDetailRecordFragment.SEAT_RECORD_TAG + ) + addToBackStack(null) } } - } + override fun onMoreRecordClick(reviewId: Int) { + viewModel.setEditReviewId(reviewId) + RecordEditDialog.newInstance(SEAT_RECORD_TAG) + .apply { show(supportFragmentManager, this.tag) } + } + + } + } + + private fun moveConfirmationDialog() { + ConfirmDeleteDialog.newInstance(SEAT_RECORD_TAG) + .apply { show(supportFragmentManager, this.tag) } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DateMonthAdapter.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DateMonthAdapter.kt index df319165..13c363e3 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DateMonthAdapter.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DateMonthAdapter.kt @@ -9,17 +9,17 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ItemDateMonthBinding -import com.depromeet.presentation.seatrecord.mockdata.MonthData +import com.depromeet.presentation.seatrecord.uiMapper.MonthUiData import com.depromeet.presentation.util.ItemDiffCallback -class DateMonthAdapter : ListAdapter( +class DateMonthAdapter : ListAdapter( ItemDiffCallback( onItemsTheSame = { oldItem, newItem -> oldItem.month == newItem.month }, onContentsTheSame = { oldItem, newItem -> oldItem == newItem } ) ) { interface OnItemMonthClickListener { - fun onItemMonthClick(item: MonthData) + fun onItemMonthClick(item: MonthUiData) } var itemMonthClickListener: OnItemMonthClickListener? = null @@ -46,7 +46,7 @@ class DateMonthAdapter : ListAdapter( class DateMonthViewHolder( private val binding: ItemDateMonthBinding, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: MonthData) { + fun bind(item: MonthUiData) { binding.tvMonth.text = when (item.month) { 0 -> "전체" else -> "${item.month}월" diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DetailRecordAdapter.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DetailRecordAdapter.kt index 50873c6d..1b3cf8c1 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DetailRecordAdapter.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/DetailRecordAdapter.kt @@ -10,20 +10,26 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import coil.load import coil.transform.CircleCropTransformation +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ItemSeatReviewDetailBinding -import com.depromeet.presentation.seatrecord.mockdata.ReviewDetailMockData +import com.depromeet.presentation.seatrecord.uiMapper.toUiKeyword +import com.depromeet.presentation.util.CalendarUtil import com.depromeet.presentation.util.ItemDiffCallback import com.depromeet.presentation.util.applyBoldSpan import com.depromeet.presentation.viewfinder.compose.KeywordFlowRow -class DetailRecordAdapter() : ListAdapter( - ItemDiffCallback( - onItemsTheSame = { oldItem, newItem -> oldItem.reviewId == newItem.reviewId }, - onContentsTheSame = { oldItem, newItem -> oldItem == newItem } - ) -) { +class TestDetailRecordAdapter( + private val myProfile: MySeatRecordResponse.MyProfileResponse, +) : + ListAdapter( + ItemDiffCallback( + onItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id }, + onContentsTheSame = { oldItem, newItem -> oldItem == newItem } + ) + ) { interface OnDetailItemClickListener { - fun onItemMoreClickListener(item: ReviewDetailMockData) + fun onItemMoreClickListener(item: MySeatRecordResponse.ReviewResponse) } var itemMoreClickListener: OnDetailItemClickListener? = null @@ -40,7 +46,7 @@ class DetailRecordAdapter() : ListAdapter( - ItemDiffCallback( - onItemsTheSame = { oldItem, newItem -> oldItem.month == newItem.month }, - onContentsTheSame = { oldItem, newItem -> oldItem == newItem } - ) -) { + +class MonthRecordAdapter() : + ListAdapter( + ItemDiffCallback( + onItemsTheSame = { oldItem, newItem -> oldItem.month == newItem.month }, + onContentsTheSame = { oldItem, newItem -> oldItem.reviews == newItem.reviews } + ) + ) { interface OnItemRecordClickListener { - fun onItemRecordClick(item: ReviewMockData) + fun onItemRecordClick(item: MySeatRecordResponse.ReviewResponse) + fun onMoreRecordClick(reviewId: Int) } var itemRecordClickListener: OnItemRecordClickListener? = null @@ -48,14 +50,18 @@ class MonthRecordViewHolder( fun bind(item: MonthReviewData) { with(binding) { adapter = RecentRecordAdapter() - tvRecentMonth.text = item.month.extractMonth(true) + "${item.month}월".also { tvRecentMonth.text = it } rvRecentPost.adapter = adapter adapter.submitList(item.reviews) adapter.itemRecordClickListener = object : RecentRecordAdapter.OnItemRecordClickListener { - override fun onItemRecordClick(item: ReviewMockData) { + override fun onItemRecordClick(item: MySeatRecordResponse.ReviewResponse) { itemRecordClickListener?.onItemRecordClick(item) } + + override fun onItemMoreClick(item: MySeatRecordResponse.ReviewResponse) { + itemRecordClickListener?.onMoreRecordClick(item.id) + } } } } diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/RecentRecordAdapter.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/RecentRecordAdapter.kt index 9d13f27d..bc29ffe0 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/RecentRecordAdapter.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/RecentRecordAdapter.kt @@ -6,23 +6,25 @@ import androidx.compose.material.MaterialTheme import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ItemRecentRecordBinding -import com.depromeet.presentation.extension.extractDay -import com.depromeet.presentation.extension.getDayOfWeek import com.depromeet.presentation.extension.loadAndClip -import com.depromeet.presentation.seatrecord.mockdata.ReviewMockData +import com.depromeet.presentation.seatrecord.uiMapper.toUiKeyword +import com.depromeet.presentation.util.CalendarUtil import com.depromeet.presentation.util.ItemDiffCallback import com.depromeet.presentation.viewfinder.compose.KeywordFlowRow class RecentRecordAdapter( -) : ListAdapter( +) : ListAdapter( ItemDiffCallback( onItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id }, onContentsTheSame = { oldItem, newItem -> oldItem == newItem } ) ) { interface OnItemRecordClickListener { - fun onItemRecordClick(item: ReviewMockData) + fun onItemRecordClick(item: MySeatRecordResponse.ReviewResponse) + fun onItemMoreClick(item: MySeatRecordResponse.ReviewResponse) } var itemRecordClickListener: OnItemRecordClickListener? = null @@ -38,9 +40,15 @@ class RecentRecordAdapter( } override fun onBindViewHolder(holder: RecentRecordViewHolder, position: Int) { - holder.bind(getItem(position)) - holder.itemView.setOnClickListener { - itemRecordClickListener?.onItemRecordClick(getItem(position)) + with(holder) { + bind(getItem(position)) + itemView.setOnClickListener { + itemRecordClickListener?.onItemRecordClick(getItem(position)) + } + binding.ibRecentStadiumMore.setOnClickListener { + itemRecordClickListener?.onItemMoreClick(getItem(position)) + } + } } } @@ -53,19 +61,23 @@ class RecentRecordViewHolder( } - fun bind(item: ReviewMockData) { + fun bind(item: MySeatRecordResponse.ReviewResponse) { with(binding) { - ivRecentImage.loadAndClip(item.image) - tvRecentDateDay.text = item.date.extractDay() - tvRecentDay.text = item.date.getDayOfWeek() - tvRecentBlockName.text = item.blockName + if (item.images.isNotEmpty()) { + ivRecentImage.loadAndClip(item.images[0].url) + } else { + ivRecentImage.loadAndClip(R.drawable.ic_image_placeholder) + } + tvRecentDateDay.text = CalendarUtil.getDayOfMonthFromDateFormat(item.date).toString() + tvRecentDay.text = CalendarUtil.getDayOfWeekFromDateFormat(item.date) + "${item.sectionName} ${item.blockName}블록".also { tvRecentBlockName.text = it } tvRecentStadiumName.text = item.stadiumName cvDetailKeyword.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { MaterialTheme { KeywordFlowRow( - keywords = item.keyword, + keywords = item.keywords.map { it.toUiKeyword() }, overflowIndex = MAX_VISIBLE_CHIPS ) } @@ -73,4 +85,5 @@ class RecentRecordViewHolder( } } } -} \ No newline at end of file + +} diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/SeatImageAdapter.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/SeatImageAdapter.kt index db7abdf1..293d7eb7 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/SeatImageAdapter.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/adapter/SeatImageAdapter.kt @@ -5,9 +5,8 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import coil.load -import coil.size.Scale import com.depromeet.presentation.databinding.ItemSeatImageBinding +import com.depromeet.presentation.extension.loadAndClip import com.depromeet.presentation.util.ItemDiffCallback class SeatImageAdapter : ListAdapter( @@ -35,6 +34,6 @@ class SeatImageViewHolder( private val binding: ItemSeatImageBinding, ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: String) { - binding.ivTest.load(item) { scale(Scale.FILL) } + binding.ivTest.loadAndClip(item) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/mockdata/SeatRecordMockData.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/mockdata/SeatRecordMockData.kt deleted file mode 100644 index b542400e..00000000 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/mockdata/SeatRecordMockData.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.depromeet.presentation.seatrecord.mockdata - -import android.os.Parcelable -import com.depromeet.presentation.extension.extractMonth -import com.depromeet.presentation.viewfinder.sample.Keyword -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.parcelize.Parcelize - -data class MonthData( - val month: Int = 0, - var isClicked: Boolean = false, -) - -data class RecordDetailMockData( - val profileDetailData: ProfileDetailData = ProfileDetailData(), - val reviews: List = emptyList(), -) - -data class ProfileDetailData( - val nickName: String= "", - val profileImage: String ="", - val level: Int =0, - val titleName: String = "", - val recordCount: Int = 0, -) - -@Parcelize -data class ReviewMockData( - val id: Int =0, - val date: String ="", - val image: String ="", - val stadiumName: String ="", - val blockName: String ="", - val keyword: List = emptyList(), -) : Parcelable - -data class MonthReviewData( - val month: String, - val reviews: List, -) - -fun List.groupByMonth(): List { - val groupedData = this.groupBy { it.date.extractMonth(false) } - return groupedData.map { (month, reviews) -> - MonthReviewData(month, reviews) - } -} - -fun makeSeatRecordData(): Flow = flow { - emit( - RecordDetailMockData( - profileDetailData = ProfileDetailData( - nickName = "노균욱", - profileImage = "https://picsum.photos/600/400", - level = 6, - titleName = "전설의 직관러", - recordCount = 37 - ), - reviews = makeRecordDetailData() - ) - ) -} - - -fun makeRecordDetailData(): List { - val list = mutableListOf() - for (i in 1..8) { - list.add( - ReviewMockData( - id = i, - date = "2024-0${i % 4 + 1}-12", - image = "https://picsum.photos/600/400", - stadiumName = "서울 잠실 야구장", - blockName = "1루 네이비석 304블록", - keyword = listOf( - Keyword("🙍‍서서 응원하는 존", 44, 0), - Keyword("☀️ 온종일 햇빛 존", 44, 1), - Keyword("🙍‍서서 응원하는 존", 44, 1), - Keyword("🙍‍서서 응원하는 존", 44, 0), - Keyword("🙍‍서서 응원하는 존", 44, 0) - ), - ) - ) - } - return list -} - -val monthList = listOf( - MonthData(0, true), - MonthData(1, false), - MonthData(2, false), - MonthData(3, false), - MonthData(4, false), - MonthData(5, false), - MonthData(6, false), - MonthData(7, false), - MonthData(8, false), - MonthData(9, false), - MonthData(10, false), - MonthData(11, false), - MonthData(12, false), -) \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/uiMapper/HomeMapper.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/uiMapper/HomeMapper.kt new file mode 100644 index 00000000..6e1d683e --- /dev/null +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/uiMapper/HomeMapper.kt @@ -0,0 +1,27 @@ +package com.depromeet.presentation.seatrecord.uiMapper + +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import com.depromeet.presentation.viewfinder.sample.Keyword + +/** + * 리팩토링 예정 + */ +data class MonthReviewData( + val month: Int, + val reviews: List, +) + +data class MonthUiData( + val month: Int = 0, + var isClicked: Boolean = false, +) + +/** + * 우선 Ui Mapper로 임시 처리 추후 관희 flowRow 변경 사항에 따라 수정 예정 + */ +fun MySeatRecordResponse.ReviewResponse.ReviewKeywordResponse.toUiKeyword() = + Keyword( + message = content, + type = if (isPositive) 1 else 0, + like = 0 + ) diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/viewmodel/SeatDetailViewModel.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/viewmodel/SeatDetailViewModel.kt deleted file mode 100644 index 4f56c821..00000000 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/viewmodel/SeatDetailViewModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.depromeet.presentation.seatrecord.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.depromeet.presentation.seatrecord.mockdata.ReviewDetailMockResult -import com.depromeet.presentation.seatrecord.mockdata.mockReviewDetailListData -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -@HiltViewModel -class SeatDetailViewModel @Inject constructor() : ViewModel() { - - private val _uiState = MutableStateFlow(ReviewDetailMockResult()) - val uiState = _uiState.asStateFlow() - - private val _deleteClickedEvent = MutableStateFlow(false) - val deleteClickedEvent = _deleteClickedEvent.asStateFlow() - - private val _editReviewId = MutableStateFlow(0) - val editReviewId = _editReviewId.asStateFlow() - - - fun getReviewData() { - mockReviewDetailListData().onEach { - _uiState.value = _uiState.value.copy(list = it) - }.launchIn(viewModelScope) - } - - - fun removeReviewData() { - val currentList = _uiState.value.list - val updatedList = currentList.filter { review -> - review.reviewId != _editReviewId.value - } - _uiState.value = _uiState.value.copy(list = updatedList) - _deleteClickedEvent.value = false - } - - fun setEditReviewId(id: Int) { - _editReviewId.value = id - } - - fun setDeleteEvent() { - _deleteClickedEvent.value = true - } -} \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/seatrecord/viewmodel/SeatRecordViewModel.kt b/presentation/src/main/java/com/depromeet/presentation/seatrecord/viewmodel/SeatRecordViewModel.kt index 6a930a63..582ab9f5 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatrecord/viewmodel/SeatRecordViewModel.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatrecord/viewmodel/SeatRecordViewModel.kt @@ -2,42 +2,139 @@ package com.depromeet.presentation.seatrecord.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.depromeet.presentation.seatrecord.mockdata.RecordDetailMockData -import com.depromeet.presentation.seatrecord.mockdata.makeSeatRecordData +import com.depromeet.core.state.UiState +import com.depromeet.domain.entity.request.home.MySeatRecordRequest +import com.depromeet.domain.entity.response.home.MySeatRecordResponse +import com.depromeet.domain.entity.response.home.ReviewDateResponse +import com.depromeet.domain.repository.HomeRepository +import com.depromeet.presentation.seatrecord.uiMapper.MonthUiData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel -class SeatRecordViewModel @Inject constructor() : ViewModel() { +class SeatRecordViewModel @Inject constructor( + private val homeRepository: HomeRepository, +) : ViewModel() { - private val _uiState = MutableStateFlow(RecordDetailMockData()) - val uiState = _uiState.asStateFlow() + private val _reviews = MutableStateFlow>(UiState.Loading) + val reviews = _reviews.asStateFlow() - private val _selectedMonth = MutableStateFlow(0) - val selectedMonth = _selectedMonth.asStateFlow() + private val _date = MutableStateFlow>(UiState.Loading) + val date = _date.asStateFlow() - private val _selectedYear = MutableStateFlow(2024) + private val _months = + MutableStateFlow>(emptyList()) + val months = _months.asStateFlow() + + private val _selectedYear = MutableStateFlow(0) + val selectedYear = _selectedYear.asStateFlow() + + private val _deleteClickedEvent = MutableStateFlow(DeleteUi.NONE) + val deleteClickedEvent = _deleteClickedEvent.asStateFlow() + + private val _editReviewId = MutableStateFlow(0) + val editReviewId = _editReviewId.asStateFlow() + + fun getReviewDate() { + viewModelScope.launch { + homeRepository.getReviewDate() + .onSuccess { data -> + if (data.yearMonths.isNotEmpty()) { + _date.value = UiState.Success(data) + _selectedYear.value = data.yearMonths[0].year + } else { + _date.value = UiState.Empty + } + } + .onFailure { e -> + _date.value = UiState.Failure(e.message ?: "실패") + } + } + } fun getSeatRecords() { - makeSeatRecordData().onEach { - _uiState.value = _uiState.value.copy( - profileDetailData = it.profileDetailData, - reviews = it.reviews - ) - }.launchIn(viewModelScope) + val month = months.value.find { it.isClicked }?.takeIf { it.month != 0 }?.month + viewModelScope.launch { + homeRepository.getMySeatRecord( + MySeatRecordRequest( + year = selectedYear.value, + month = month + ) + ).onSuccess { data -> + Timber.d("GET_SEAT_RECORDS_TEST SUCCESS : $data") + _reviews.value = UiState.Success(data) + }.onFailure { + Timber.d("GET_SEAT_RECORDS_TEST FAIL : $it") + _reviews.value = UiState.Failure(it.message ?: "실패") + } + } } fun setSelectedYear(year: Int) { _selectedYear.value = year } + fun initMonths() { + val currentState = date.value + if (currentState is UiState.Success) { + val selectedYearMonths = + currentState.data.yearMonths.first { it.year == selectedYear.value }.months + _months.value = selectedYearMonths.mapIndexed { index, month -> + MonthUiData(month = month, isClicked = index == 0) + } + } + } + fun setSelectedMonth(month: Int) { - _selectedMonth.value = month + _months.value = months.value.map { + it.copy(isClicked = it.month == month) + } + } + + fun setEditReviewId(id: Int) { + _editReviewId.value = id } + fun setDeleteEvent(deleteUi: DeleteUi) { + _deleteClickedEvent.value = deleteUi + } + + fun cancelDeleteEvent() { + _deleteClickedEvent.value = DeleteUi.NONE + } + + fun removeReviewData() { + val currentState = reviews.value + if (currentState is UiState.Success) { + viewModelScope.launch { + homeRepository.deleteReview(editReviewId.value) + .onSuccess { + if (it.reviewId == editReviewId.value) { + val updatedList = currentState.data.reviews.filter { review -> + review.id != editReviewId.value + } + _reviews.value = + UiState.Success(currentState.data.copy(reviews = updatedList)) + } + } + .onFailure { + Timber.d("삭제 실패 : $it") + } + } + _deleteClickedEvent.value = DeleteUi.NONE + } + + } + +} + +enum class DeleteUi { + NONE, + SEAT_RECORD, + SEAT_DETAIL } \ No newline at end of file diff --git a/presentation/src/main/java/com/depromeet/presentation/util/CalenderUtil.kt b/presentation/src/main/java/com/depromeet/presentation/util/CalenderUtil.kt index 8b6ba296..b117e595 100644 --- a/presentation/src/main/java/com/depromeet/presentation/util/CalenderUtil.kt +++ b/presentation/src/main/java/com/depromeet/presentation/util/CalenderUtil.kt @@ -1,11 +1,16 @@ package com.depromeet.presentation.util import java.text.SimpleDateFormat +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.format.TextStyle import java.util.Calendar import java.util.Locale object CalendarUtil { + private const val ISO_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" private const val DATE_FORMAT = "yy.MM.dd" fun formatCalendarDate(calendar: Calendar): String { @@ -33,4 +38,35 @@ object CalendarUtil { fun getDay(calendar: Calendar): Int { return calendar.get(Calendar.DAY_OF_MONTH) } + + fun getMonthFromDateFormat(date: String): Int { + val formatter = DateTimeFormatter.ofPattern(ISO_DATE_FORMAT) + val parsedDate = LocalDateTime.parse(date, formatter) + return parsedDate.monthValue + } + + fun getDayOfMonthFromDateFormat(date: String): Int { + val formatter = DateTimeFormatter.ofPattern(ISO_DATE_FORMAT) + val parsedDate = LocalDateTime.parse(date, formatter) + return parsedDate.dayOfMonth + } + + fun getDayOfWeekFromDateFormat(date: String): String { + val formatter = DateTimeFormatter.ofPattern(ISO_DATE_FORMAT) + val parsedDate = LocalDateTime.parse(date, formatter) + return parsedDate.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) + } + + fun getFormattedDate(date: String): String { + val formatter = DateTimeFormatter.ofPattern(ISO_DATE_FORMAT) + val parsedDate = LocalDateTime.parse(date, formatter) + val outputFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일", Locale.KOREAN) + return parsedDate.format(outputFormatter) + } + + fun getCurrentYear(): Int { + val today = LocalDate.now() + return today.year + } + } diff --git a/presentation/src/main/res/drawable/circlr_gray900_fill.xml b/presentation/src/main/res/drawable/circlr_gray900_fill.xml new file mode 100644 index 00000000..e480c00d --- /dev/null +++ b/presentation/src/main/res/drawable/circlr_gray900_fill.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_camera_14.xml b/presentation/src/main/res/drawable/ic_camera_14.xml new file mode 100644 index 00000000..8625f5d3 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_camera_14.xml @@ -0,0 +1,10 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_default_profile.png b/presentation/src/main/res/drawable/ic_default_profile.png index 3e3800f4..f8a650b9 100644 Binary files a/presentation/src/main/res/drawable/ic_default_profile.png and b/presentation/src/main/res/drawable/ic_default_profile.png differ diff --git a/presentation/src/main/res/drawable/ic_image_error.png b/presentation/src/main/res/drawable/ic_image_error.png new file mode 100644 index 00000000..2873ba61 Binary files /dev/null and b/presentation/src/main/res/drawable/ic_image_error.png differ diff --git a/presentation/src/main/res/drawable/ic_image_placeholder.jpg b/presentation/src/main/res/drawable/ic_image_placeholder.jpg new file mode 100644 index 00000000..d4e09b35 Binary files /dev/null and b/presentation/src/main/res/drawable/ic_image_placeholder.jpg differ diff --git a/presentation/src/main/res/layout/activity_home.xml b/presentation/src/main/res/layout/activity_home.xml index 3142084e..ea7e64ea 100644 --- a/presentation/src/main/res/layout/activity_home.xml +++ b/presentation/src/main/res/layout/activity_home.xml @@ -51,7 +51,8 @@ android:layout_marginTop="32dp" android:src="@drawable/ic_default_profile" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" /> + app:layout_constraintEnd_toEndOf="@id/iv_home_profile" + tools:ignore="contentDescription" /> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" /> + app:layout_constraintTop_toTopOf="@id/tv_home_find_sight" + tools:ignore="contentDescription" /> + app:layout_constraintEnd_toEndOf="parent" + tools:ignore="contentDescription" /> @@ -169,19 +175,22 @@ android:layout_height="wrap_content" android:layout_marginTop="24dp" android:background="@drawable/rect_white_fill_top20" - android:paddingBottom="80dp" + android:paddingBottom="60dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/cl_main_sight"> + app:layout_constraintTop_toBottomOf="@id/cl_main_find_sight"> @@ -203,7 +212,8 @@ android:src="@drawable/ic_chevron_right" app:layout_constraintBottom_toBottomOf="@id/tv_home_register_sight" app:layout_constraintStart_toEndOf="@id/tv_home_register_sight" - app:layout_constraintTop_toTopOf="@id/tv_home_register_sight" /> + app:layout_constraintTop_toTopOf="@id/tv_home_register_sight" + tools:ignore="contentDescription" /> + app:layout_constraintEnd_toEndOf="parent" + tools:ignore="contentDescription" /> + app:layout_constraintTop_toTopOf="@id/tv_home_sight_record" + tools:ignore="contentDescription" /> @@ -335,7 +351,8 @@ android:src="@drawable/ic_folder_blank" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" /> @@ -390,6 +408,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" tools:src="@drawable/ic_stadium_test" /> @@ -430,6 +450,7 @@ app:layout_constraintEnd_toStartOf="@+id/iv_home_recent_two_record2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" tools:src="@drawable/ic_stadium_test" /> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" /> @@ -482,6 +505,7 @@ app:layout_constraintEnd_toStartOf="@+id/iv_home_recent_three_record2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" tools:src="@drawable/ic_stadium_test" /> + app:layout_constraintTop_toTopOf="@id/iv_home_recent_three_record1" + tools:ignore="contentDescription" /> + app:layout_constraintTop_toBottomOf="@id/iv_home_recent_three_record2" + tools:ignore="contentDescription" /> diff --git a/presentation/src/main/res/layout/activity_profile_edit.xml b/presentation/src/main/res/layout/activity_profile_edit.xml index 4d8d6bf7..b624de5c 100644 --- a/presentation/src/main/res/layout/activity_profile_edit.xml +++ b/presentation/src/main/res/layout/activity_profile_edit.xml @@ -33,7 +33,8 @@ android:background="@android:color/transparent" android:src="@drawable/ic_x_close" app:layout_constraintStart_toEndOf="@id/gl_start" - app:layout_constraintTop_toBottomOf="@id/gl_top" /> + app:layout_constraintTop_toBottomOf="@id/gl_top" + tools:ignore="contentDescription" /> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="contentDescription" + tools:src="@drawable/ic_default_profile" /> + app:layout_constraintEnd_toEndOf="@id/iv_profile_edit_image" + tools:ignore="contentDescription" /> diff --git a/presentation/src/main/res/layout/activity_seat_detail_record.xml b/presentation/src/main/res/layout/activity_seat_detail_record.xml index 37a885b2..2cb90480 100644 --- a/presentation/src/main/res/layout/activity_seat_detail_record.xml +++ b/presentation/src/main/res/layout/activity_seat_detail_record.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="@color/white"> + + + + + - - - @@ -297,5 +270,39 @@ + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_record_edit_bottom_sheet.xml b/presentation/src/main/res/layout/fragment_record_edit_bottom_sheet.xml index 66ebc21d..7829f035 100644 --- a/presentation/src/main/res/layout/fragment_record_edit_bottom_sheet.xml +++ b/presentation/src/main/res/layout/fragment_record_edit_bottom_sheet.xml @@ -5,34 +5,22 @@ android:layout_height="wrap_content" android:background="@drawable/rect_white_fill_top20"> - - + app:layout_constraintBottom_toBottomOf="parent" /> \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_baseball_team.xml b/presentation/src/main/res/layout/item_baseball_team.xml index 3315c998..aa061494 100644 --- a/presentation/src/main/res/layout/item_baseball_team.xml +++ b/presentation/src/main/res/layout/item_baseball_team.xml @@ -4,28 +4,32 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="78dp" - android:background="@drawable/rect_gray100_line_10" - android:paddingHorizontal="20dp" - android:paddingVertical="24dp"> + android:background="@drawable/rect_gray100_line_10"> diff --git a/presentation/src/main/res/layout/item_recent_record.xml b/presentation/src/main/res/layout/item_recent_record.xml index 94fe7bba..d6df2e99 100644 --- a/presentation/src/main/res/layout/item_recent_record.xml +++ b/presentation/src/main/res/layout/item_recent_record.xml @@ -14,17 +14,16 @@ android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:text="12" /> + tools:text="1" /> @@ -46,6 +45,18 @@ tools:ignore="contentDescription" tools:src="@drawable/ic_stadium_test" /> + + 더보기 아직 작성한 시야 기록이 없어요. 시야 기록 남기기 + + 직관 첫 걸음 앨범에서 선택 @@ -25,12 +27,15 @@ 이미 사용중인 닉네임이에요. 닉네임은 알파벳 대소문자, 숫자, 한글만 입력 가능합니다. 닉네임은 최소 2글자에서 최대 10글자로 입력해 주세요. + 비어있습니다. 정상적으로 입력 완료! 선택한 사진\n용량이 너무 커요 5MB에 맞게 다시 선택해주세요 15MB에 맞게 다시 선택해주세요 + 선택한 사진\n확장자가 맞지 않아요 + jpg, jpeg, png 사진을 선택해주세요 확인 @@ -55,6 +60,12 @@ 게시물 삭제시 복구가 불가능합니다 삭제하기 취소 + 경기장 탐험가 + 직관의 여유 + 응원 단장 + 야구정 VIP + 전설의 직관러 + 알 수 없음 \ No newline at end of file