diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/DataSourceModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/DataSourceModule.kt index 1bc750e..99fa87a 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/DataSourceModule.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/DataSourceModule.kt @@ -1,10 +1,14 @@ package com.hackathon.alddeul_babsang.app.di import com.hackathon.alddeul_babsang.data.datasource.BabsangDataSource +import com.hackathon.alddeul_babsang.data.datasource.DetailDataSource import com.hackathon.alddeul_babsang.data.datasource.ExampleDataSource +import com.hackathon.alddeul_babsang.data.datasource.ProfileDataSource import com.hackathon.alddeul_babsang.data.datasource.UserPreferencesDataSource import com.hackathon.alddeul_babsang.data.datasourceimpl.BabsangDataSourceImpl +import com.hackathon.alddeul_babsang.data.datasourceimpl.DetailDataSourceImpl import com.hackathon.alddeul_babsang.data.datasourceimpl.ExampleDataSourceImpl +import com.hackathon.alddeul_babsang.data.datasourceimpl.ProfileDataSourceImpl import com.hackathon.alddeul_babsang.data.datasourceimpl.UserPreferencesDataSourceImpl import dagger.Binds import dagger.Module @@ -27,4 +31,12 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindBabsangDataSource(babsangDataSourceImpl: BabsangDataSourceImpl): BabsangDataSource + + @Binds + @Singleton + abstract fun bindProfileDataSource(profileDataSourceImpl: ProfileDataSourceImpl): ProfileDataSource + + @Binds + @Singleton + abstract fun bindDetailDataSource(detailDataSourceImpl: DetailDataSourceImpl): DetailDataSource } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RepositoryModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RepositoryModule.kt index 0db5c38..8fd7c27 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RepositoryModule.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/RepositoryModule.kt @@ -1,10 +1,14 @@ package com.hackathon.alddeul_babsang.app.di import com.hackathon.alddeul_babsang.data.repositoryimpl.BabsangRepositoryImpl +import com.hackathon.alddeul_babsang.data.repositoryimpl.DetailRepositoryImpl import com.hackathon.alddeul_babsang.data.repositoryimpl.ExampleRepositoryImpl +import com.hackathon.alddeul_babsang.data.repositoryimpl.ProfileRepositoryImpl import com.hackathon.alddeul_babsang.data.repositoryimpl.UserPreferencesRepositoryImpl import com.hackathon.alddeul_babsang.domain.repository.BabsangRepository +import com.hackathon.alddeul_babsang.domain.repository.DetailRepository import com.hackathon.alddeul_babsang.domain.repository.ExampleRepository +import com.hackathon.alddeul_babsang.domain.repository.ProfileRepository import com.hackathon.alddeul_babsang.domain.repository.UserPreferencesRepository import dagger.Binds import dagger.Module @@ -27,4 +31,12 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindBabsangRepository(babsangRepositoryImpl: BabsangRepositoryImpl): BabsangRepository + + @Binds + @Singleton + abstract fun bindProfileRepository(profileRepositoryImpl: ProfileRepositoryImpl): ProfileRepository + + @Binds + @Singleton + abstract fun bindDetailRepository(detailRepositoryImpl: DetailRepositoryImpl): DetailRepository } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/ServiceModule.kt b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/ServiceModule.kt index 27583ee..e10276a 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/app/di/ServiceModule.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/app/di/ServiceModule.kt @@ -1,6 +1,8 @@ package com.hackathon.alddeul_babsang.app.di import com.hackathon.alddeul_babsang.data.service.BabsangApiService +import com.hackathon.alddeul_babsang.data.service.DetailApiService +import com.hackathon.alddeul_babsang.data.service.ProfileApiService import com.sopt.data.service.ExampleApiService import dagger.Module import dagger.Provides @@ -24,4 +26,16 @@ object ServiceModule { fun provideBabsangService( @AlddeulRetrofit retrofit: Retrofit ): BabsangApiService = retrofit.create(BabsangApiService::class.java) + + @Provides + @Singleton + fun provideProfileService( + @AlddeulRetrofit retrofit: Retrofit + ): ProfileApiService = retrofit.create(ProfileApiService::class.java) + + @Provides + @Singleton + fun provideDetailService( + @AlddeulRetrofit retrofit: Retrofit + ): DetailApiService = retrofit.create(DetailApiService::class.java) } diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/DetailDataSource.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/DetailDataSource.kt new file mode 100644 index 0000000..38646b0 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/DetailDataSource.kt @@ -0,0 +1,14 @@ +package com.hackathon.alddeul_babsang.data.datasource + +import com.hackathon.alddeul_babsang.data.dto.BaseResponse +import com.hackathon.alddeul_babsang.data.dto.response.ResponseReviewDto +import okhttp3.MultipartBody +import okhttp3.RequestBody + +interface DetailDataSource { + suspend fun postReview( + storeId: Long, + data: Map, // JSON 데이터를 포함하는 Map + reviewImage: MultipartBody.Part? = null // 이미지 파일 + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/ProfileDataSource.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/ProfileDataSource.kt new file mode 100644 index 0000000..87e2dbb --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasource/ProfileDataSource.kt @@ -0,0 +1,15 @@ +package com.hackathon.alddeul_babsang.data.datasource + +import com.hackathon.alddeul_babsang.data.dto.BaseResponse +import com.hackathon.alddeul_babsang.data.dto.request.RequestLikesDto +import com.hackathon.alddeul_babsang.data.dto.response.ResponseLikesDto + +interface ProfileDataSource { + suspend fun getLikes( + userId: Long + ): BaseResponse + + suspend fun postLike( + requestLikesDto: RequestLikesDto + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/DetailDataSourceImpl.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/DetailDataSourceImpl.kt new file mode 100644 index 0000000..74769ba --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/DetailDataSourceImpl.kt @@ -0,0 +1,22 @@ +package com.hackathon.alddeul_babsang.data.datasourceimpl + +import com.hackathon.alddeul_babsang.data.datasource.DetailDataSource +import com.hackathon.alddeul_babsang.data.dto.BaseResponse +import com.hackathon.alddeul_babsang.data.dto.response.ResponseReviewDto +import com.hackathon.alddeul_babsang.data.service.DetailApiService +import okhttp3.MultipartBody +import okhttp3.RequestBody +import javax.inject.Inject + +class DetailDataSourceImpl @Inject constructor( + private val detailApiService: DetailApiService +) : DetailDataSource { + override suspend fun postReview( + storeId: Long, + data: Map, + reviewImage: MultipartBody.Part? + ): BaseResponse { + return detailApiService.postReview(storeId, data, reviewImage) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/ProfileDataSourceImpl.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/ProfileDataSourceImpl.kt new file mode 100644 index 0000000..4295b8e --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/datasourceimpl/ProfileDataSourceImpl.kt @@ -0,0 +1,22 @@ +package com.hackathon.alddeul_babsang.data.datasourceimpl + +import com.hackathon.alddeul_babsang.data.datasource.ProfileDataSource +import com.hackathon.alddeul_babsang.data.dto.BaseResponse +import com.hackathon.alddeul_babsang.data.dto.request.RequestLikesDto +import com.hackathon.alddeul_babsang.data.dto.response.ResponseLikesDto +import com.hackathon.alddeul_babsang.data.service.ProfileApiService +import javax.inject.Inject + +class ProfileDataSourceImpl @Inject constructor( + private val profileApiService: ProfileApiService +) : ProfileDataSource { + override suspend fun getLikes( + userId: Long + ): BaseResponse { + return profileApiService.getLikes(userId) + } + + override suspend fun postLike(requestLikesDto: RequestLikesDto): BaseResponse { + return profileApiService.postLike(requestLikesDto) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/request/RequestLikesDto.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/request/RequestLikesDto.kt new file mode 100644 index 0000000..a33787a --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/request/RequestLikesDto.kt @@ -0,0 +1,10 @@ +package com.hackathon.alddeul_babsang.data.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestLikesDto ( + @SerialName("userId") val userId: Long, + @SerialName("storeId") val storeId: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/request/gitkeep b/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/request/gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/response/ResponseLikesDto.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/response/ResponseLikesDto.kt new file mode 100644 index 0000000..a173239 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/response/ResponseLikesDto.kt @@ -0,0 +1,20 @@ +package com.hackathon.alddeul_babsang.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseLikesDto( + @SerialName("favoriteStoreDetailDtos") val favoriteRestaurants: List +) + +@Serializable +data class FavoriteRestaurantDto( + @SerialName("restaurantId") val restaurantId: Long, + @SerialName("name") val name: String, + @SerialName("category") val category: String, + @SerialName("address") val address: String, + @SerialName("contact") val contact: String, + @SerialName("restaurantImageUrl") val restaurantImageUrl: String? = null, + @SerialName("favorite") val favorite: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/response/ResponseReviewDto.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/response/ResponseReviewDto.kt new file mode 100644 index 0000000..22b73c1 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/dto/response/ResponseReviewDto.kt @@ -0,0 +1,11 @@ +package com.hackathon.alddeul_babsang.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseReviewDto ( + @SerialName("storeId") val storeId: Long, + @SerialName("userId") val userId: Long, + @SerialName("message") val message: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/DetailRepositoryImpl.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/DetailRepositoryImpl.kt new file mode 100644 index 0000000..2ac1915 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/DetailRepositoryImpl.kt @@ -0,0 +1,43 @@ +package com.hackathon.alddeul_babsang.data.repositoryimpl + +import com.hackathon.alddeul_babsang.data.datasource.DetailDataSource +import com.hackathon.alddeul_babsang.domain.repository.DetailRepository +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.io.File +import javax.inject.Inject + +class DetailRepositoryImpl @Inject constructor( + private val detailDataSource: DetailDataSource +) : DetailRepository { + override suspend fun postReview( + storeId: Long, + userId: Long, + rating: Double, + content: String, + reviewImage: File + ): Result { + return runCatching { + val dataMap = mapOf( + "userId" to userId.toString().toRequestBody("text/plain".toMediaTypeOrNull()), + "rating" to rating.toString().toRequestBody("text/plain".toMediaTypeOrNull()), + "content" to content.toRequestBody("text/plain".toMediaTypeOrNull()) + ) + + // 이미지 파일 파트 생성 + val filePart = reviewImage?.let { + val requestBody = it.asRequestBody("image/jpeg".toMediaTypeOrNull()) + MultipartBody.Part.createFormData("reviewImage", it.name, requestBody) + } + + detailDataSource.postReview( + storeId = storeId, + data = dataMap, + reviewImage = filePart + ).message + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/ProfileRepositoryImpl.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/ProfileRepositoryImpl.kt new file mode 100644 index 0000000..ca0145c --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/repositoryimpl/ProfileRepositoryImpl.kt @@ -0,0 +1,32 @@ +package com.hackathon.alddeul_babsang.data.repositoryimpl + +import com.hackathon.alddeul_babsang.data.datasource.ProfileDataSource +import com.hackathon.alddeul_babsang.data.dto.request.RequestLikesDto +import com.hackathon.alddeul_babsang.data.dto.response.FavoriteRestaurantDto +import com.hackathon.alddeul_babsang.domain.repository.ProfileRepository +import javax.inject.Inject + +class ProfileRepositoryImpl @Inject constructor( + private val profileDataSource: ProfileDataSource +) : ProfileRepository { + override suspend fun getLikes( + userId: Long, + ): Result> { + return runCatching { + profileDataSource.getLikes( + userId = userId + ).result?.favoriteRestaurants ?: emptyList() + } + } + + override suspend fun postLike(userId: Long, storeId: Long): Result { + return runCatching { + profileDataSource.postLike( + requestLikesDto = RequestLikesDto( + userId = userId, + storeId = storeId + ) + ).result.toString() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/service/ApiKeyStorage.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/service/ApiKeyStorage.kt index 1e31f95..20eec7a 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/data/service/ApiKeyStorage.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/service/ApiKeyStorage.kt @@ -18,4 +18,6 @@ object ApiKeyStorage { const val DELETE_ACCOUNT = "delete-account" const val MYPAGE = "mypage" const val LOGOUT = "logout" + const val USER_ID = "userId" + const val STORE_ID = "storeId" } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/service/DetailApiService.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/service/DetailApiService.kt new file mode 100644 index 0000000..395383d --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/service/DetailApiService.kt @@ -0,0 +1,23 @@ +package com.hackathon.alddeul_babsang.data.service + +import com.hackathon.alddeul_babsang.data.dto.BaseResponse +import com.hackathon.alddeul_babsang.data.dto.response.ResponseReviewDto +import com.sopt.data.service.ApiKeyStorage.REVIEW +import com.sopt.data.service.ApiKeyStorage.STORE_ID +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.PartMap +import retrofit2.http.Path + +interface DetailApiService { + @Multipart + @POST("/$REVIEW/{$STORE_ID}") + suspend fun postReview( + @Path("storeId") storeId: Long, + @PartMap data: Map, + @Part reviewImage: MultipartBody.Part? = null + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/data/service/ProfileApiService.kt b/app/src/main/java/com/hackathon/alddeul_babsang/data/service/ProfileApiService.kt new file mode 100644 index 0000000..886aeb3 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/data/service/ProfileApiService.kt @@ -0,0 +1,23 @@ +package com.hackathon.alddeul_babsang.data.service + +import com.hackathon.alddeul_babsang.data.dto.BaseResponse +import com.hackathon.alddeul_babsang.data.dto.request.RequestLikesDto +import com.hackathon.alddeul_babsang.data.dto.response.ResponseLikesDto +import com.sopt.data.service.ApiKeyStorage.FAVORITES +import com.sopt.data.service.ApiKeyStorage.USER_ID +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface ProfileApiService { + @GET("/$FAVORITES/{$USER_ID}}") + suspend fun getLikes( + @Path("userId") userId: Long, + ): BaseResponse + + @POST("/$FAVORITES") + suspend fun postLike( + @Body requestLikesDto: RequestLikesDto + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/DetailRepository.kt b/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/DetailRepository.kt new file mode 100644 index 0000000..ffd6040 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/DetailRepository.kt @@ -0,0 +1,13 @@ +package com.hackathon.alddeul_babsang.domain.repository + +import java.io.File + +interface DetailRepository { + suspend fun postReview( + storeId: Long, + userId: Long, + rating: Double, + content: String, + reviewImage: File + ): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/ProfileRepository.kt b/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/ProfileRepository.kt new file mode 100644 index 0000000..51f5222 --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/domain/repository/ProfileRepository.kt @@ -0,0 +1,14 @@ +package com.hackathon.alddeul_babsang.domain.repository + +import com.hackathon.alddeul_babsang.data.dto.response.FavoriteRestaurantDto + +interface ProfileRepository { + suspend fun getLikes( + userId: Long, + ): Result> + + suspend fun postLike( + userId: Long, + storeId: Long, + ): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/babsang/screen/BabsangItem.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/babsang/screen/BabsangItem.kt index fe9e5ff..bac5a52 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/babsang/screen/BabsangItem.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/babsang/screen/BabsangItem.kt @@ -159,22 +159,4 @@ fun ReplaceImage2(codeName: String, imageUrl: String?) { ) } } -} - -@Preview(showBackground = true) -@Composable -fun BabsangListItemPreview() { - AlddeulBabsangTheme { - LikeItem( - data = LikesEntity( - id = 1, - avatar = null, - name = "송이네 밥상", - codeName = "경양식/일식", - address = "서울특별시 용산구 청파동 11", - phone = "02-210-0220", - favorite = true - ) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewScreen.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewScreen.kt index fb8b0bb..6446db3 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewScreen.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -39,6 +40,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.rememberAsyncImagePainter import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.hackathon.alddeul_babsang.R @@ -55,15 +58,36 @@ import com.hackathon.alddeul_babsang.core_ui.theme.Yellow import com.hackathon.alddeul_babsang.core_ui.theme.body4Regular import com.hackathon.alddeul_babsang.core_ui.theme.head4Bold import com.hackathon.alddeul_babsang.presentation.detail.navigation.DetailNavigator +import com.hackathon.alddeul_babsang.util.UiState import com.hackathon.alddeul_babsang.util.toast +import com.hackathon.alddeul_babsang.util.uriToFile +import timber.log.Timber @Composable fun ReviewRoute( navigator: DetailNavigator, id: Long, ) { + val reviewViewModel: ReviewViewModel = hiltViewModel() val keyboardController = LocalSoftwareKeyboardController.current val systemUiController = rememberSystemUiController() + val postReviewState by reviewViewModel.postReviewState.collectAsStateWithLifecycle(UiState.Empty) + + when (postReviewState) { + is UiState.Success -> { + navigator.navigateBack() + keyboardController?.hide() + Timber.d("Review post success") + } + + is UiState.Failure -> { + val message = (postReviewState as UiState.Failure).msg + Timber.e("Review post failed: $message") + LocalContext.current.toast(message) + } + + else -> {} + } SideEffect { systemUiController.setStatusBarColor( @@ -72,19 +96,18 @@ fun ReviewRoute( } ReviewScreen( + id = id, onBackClick = { navigator.navigateBack() }, - onCompleteClick = { - keyboardController?.hide() - navigator.navigateBack() - } + reviewViewModel = reviewViewModel ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReviewScreen( + id: Long = 0, onBackClick: () -> Unit = {}, - onCompleteClick: () -> Unit = {} + reviewViewModel: ReviewViewModel ) { var imageUri by remember { mutableStateOf(null) } val galleryLauncher = rememberLauncherForActivityResult( @@ -95,6 +118,7 @@ fun ReviewScreen( ) var reviewLength by remember { mutableStateOf("") } val context = LocalContext.current + var rating by remember { mutableIntStateOf(0) } Scaffold( topBar = { @@ -158,7 +182,10 @@ fun ReviewScreen( } Spacer(modifier = Modifier.height(30.dp)) AlddeulHeader(text = R.string.tv_review_rating) - StarRating() + StarRating( + rating = rating, + onRatingChange = { rating = it } + ) AlddeulHeader(text = R.string.tv_review_detail) ReviewTextField( value = reviewLength, @@ -177,8 +204,16 @@ fun ReviewScreen( AlddeulButton( text = R.string.btn_review_complete, onClick = { - if (reviewLength.length <= 100) onCompleteClick() - else context.toast(context.getString(R.string.toast_review_length)) + if (reviewLength.length <= 100 && imageUri != null) { + val file = uriToFile(imageUri!!, context) + reviewViewModel.postReview( + storeId = id, + userId = 1, + rating = rating.toDouble(), + content = reviewLength, + reviewImage = file + ) + } else context.toast(context.getString(R.string.toast_review_length)) } ) } @@ -186,21 +221,23 @@ fun ReviewScreen( } @Composable -fun StarRating() { +fun StarRating( + rating: Int, // 현재 선택된 별 개수 + onRatingChange: (Int) -> Unit // 별 개수 변경 시 호출되는 함수 +) { val starCount = 5 - var selectedStars by remember { mutableStateOf(List(starCount) { false }) } LazyRow { items(starCount) { index -> IconButton( onClick = { - selectedStars = List(selectedStars.size) { i -> i <= index } + onRatingChange(index + 1) // 클릭된 별까지의 개수를 전달 } ) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_review_star), contentDescription = null, - tint = if (selectedStars[index]) Yellow else Gray100 + tint = if (index < rating) Yellow else Gray100 ) } } @@ -211,6 +248,8 @@ fun StarRating() { @Composable fun ReviewScreenPreview() { AlddeulBabsangTheme { - ReviewScreen() + ReviewScreen( + reviewViewModel = hiltViewModel() + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewViewModel.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewViewModel.kt new file mode 100644 index 0000000..309897c --- /dev/null +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/detail/screen/ReviewViewModel.kt @@ -0,0 +1,38 @@ +package com.hackathon.alddeul_babsang.presentation.detail.screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hackathon.alddeul_babsang.domain.repository.DetailRepository +import com.hackathon.alddeul_babsang.util.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import java.io.File +import javax.inject.Inject + +@HiltViewModel +class ReviewViewModel @Inject constructor( + private val detailRepository: DetailRepository +) : ViewModel() { + private val _postReviewState = MutableStateFlow>(UiState.Empty) + val postReviewState: StateFlow> = _postReviewState + + fun postReview( + storeId: Long, + userId: Long, + rating: Double, + content: String, + reviewImage: File + ) = viewModelScope.launch { + _postReviewState.emit(UiState.Loading) + detailRepository.postReview(storeId, userId, rating, content, reviewImage).fold( + onSuccess = { + _postReviewState.emit(UiState.Success(it)) + }, + onFailure = { + _postReviewState.emit(UiState.Failure(it.message.toString())) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeItem.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeItem.kt index 012fcb1..3057997 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeItem.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeItem.kt @@ -37,12 +37,13 @@ import com.hackathon.alddeul_babsang.core_ui.theme.Orange900 import com.hackathon.alddeul_babsang.core_ui.theme.body2Regular import com.hackathon.alddeul_babsang.core_ui.theme.body4Regular import com.hackathon.alddeul_babsang.core_ui.theme.head4Bold +import com.hackathon.alddeul_babsang.data.dto.response.FavoriteRestaurantDto import com.hackathon.alddeul_babsang.domain.entity.LikesEntity @Composable fun LikeItem( onClick: () -> Unit = {}, - data: LikesEntity + data: FavoriteRestaurantDto ) { var isFavorite by remember { mutableStateOf(data.favorite) } @@ -70,7 +71,7 @@ fun LikeItem( .clip(RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)) ) { // AsyncImage 로드 - LoadImageWithPlaceholder(data.codeName, data.avatar) + LoadImageWithPlaceholder(data.category, data.restaurantImageUrl) Image( painter = painterResource(heartIconId), contentDescription = null, @@ -93,7 +94,7 @@ fun LikeItem( ) Spacer(modifier = Modifier.width(15.dp)) Text( - text = data.codeName, + text = data.category, style = body2Regular, color = Orange800, modifier = Modifier @@ -110,7 +111,7 @@ fun LikeItem( ) Spacer(modifier = Modifier.height(7.dp)) Text( - text = data.phone, + text = data.contact, style = body4Regular, color = Gray300, modifier = Modifier.padding(start = 20.dp, bottom = 20.dp) @@ -157,22 +158,4 @@ fun LoadImageWithPlaceholder(codeName: String, imageUrl: String?) { ) } } -} - -@Preview(showBackground = true) -@Composable -fun LikeItemPreview() { - AlddeulBabsangTheme { - LikeItem( - data = LikesEntity( - id = 1, - avatar = null, - name = "송이네 밥상", - codeName = "경양식/일식", - address = "서울특별시 용산구 청파동 11", - phone = "02-210-0220", - favorite = true - ) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeScreen.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeScreen.kt index b1f9836..fc1f23c 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeScreen.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -15,7 +14,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -26,8 +27,10 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.hackathon.alddeul_babsang.R +import com.hackathon.alddeul_babsang.core_ui.component.LoadingCircleIndicator import com.hackathon.alddeul_babsang.core_ui.theme.AlddeulBabsangTheme import com.hackathon.alddeul_babsang.core_ui.theme.Gray900 import com.hackathon.alddeul_babsang.core_ui.theme.Orange800 @@ -35,6 +38,7 @@ import com.hackathon.alddeul_babsang.core_ui.theme.White import com.hackathon.alddeul_babsang.core_ui.theme.head4Bold import com.hackathon.alddeul_babsang.core_ui.theme.head6Semi import com.hackathon.alddeul_babsang.presentation.profile.navigation.ProfileNavigator +import com.hackathon.alddeul_babsang.util.UiState @Composable fun LikeRoute( @@ -50,6 +54,10 @@ fun LikeRoute( ) } + LaunchedEffect(Unit) { + likeViewModel.getLikes() + } + LikeScreen( onItemClick = { id -> navigator.navigateDetail(id) }, onBackClick = { navigator.navigateBack() }, @@ -64,7 +72,7 @@ fun LikeScreen( onBackClick: () -> Unit = {}, likeViewModel: LikeViewModel ) { - val scrollState = rememberScrollState() + val getLikesState by likeViewModel.getLikesState.collectAsStateWithLifecycle(UiState.Empty) Scaffold( topBar = { @@ -114,11 +122,29 @@ fun LikeScreen( style = head6Semi ) } - items(likeViewModel.mockLikes) { item -> - LikeItem( - onClick = { onItemClick(item.id) }, - data = item - ) + when (getLikesState) { + is UiState.Success -> { + items((getLikesState as UiState.Success).data) { item -> + LikeItem( + onClick = { onItemClick(item.restaurantId) }, + data = item + ) + } + } + + is UiState.Loading -> { + item { + LoadingCircleIndicator() + } + } + + is UiState.Failure -> { + item { + Text(text = (getLikesState as UiState.Failure).msg) + } + } + + else -> {} } } } diff --git a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeViewModel.kt b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeViewModel.kt index 00be6eb..528cd0c 100644 --- a/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeViewModel.kt +++ b/app/src/main/java/com/hackathon/alddeul_babsang/presentation/profile/screen/LikeViewModel.kt @@ -2,12 +2,37 @@ package com.hackathon.alddeul_babsang.presentation.profile.screen import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hackathon.alddeul_babsang.data.dto.response.FavoriteRestaurantDto import com.hackathon.alddeul_babsang.domain.entity.LikesEntity +import com.hackathon.alddeul_babsang.domain.repository.ProfileRepository +import com.hackathon.alddeul_babsang.util.UiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class LikeViewModel @Inject constructor() : ViewModel() { +class LikeViewModel @Inject constructor( + private val profileRepository: ProfileRepository +) : ViewModel() { + private val _getLikesState = + MutableStateFlow>>(UiState.Empty) + val getLikesState: StateFlow>> = _getLikesState + + fun getLikes() = viewModelScope.launch { + _getLikesState.emit(UiState.Loading) + profileRepository.getLikes(userId = 1).fold( + onSuccess = { + _getLikesState.emit(UiState.Success(it)) + }, + onFailure = { + _getLikesState.emit(UiState.Failure(it.message.toString())) + } + ) + } + val mockLikes = listOf( LikesEntity( id = 1,