From 86fe8037db4e1c83ae3ffe3cf25b4e7750b6ed7e Mon Sep 17 00:00:00 2001 From: JiWoo1261 Date: Wed, 16 Jul 2025 19:54:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat/#102=20:=20=EA=B3=B5=ED=86=B5=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20like=20?= =?UTF-8?q?=EC=BD=9C=EB=B0=B1=EC=9D=84=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/CourseCard.kt | 17 ++++-- .../designsystem/component/CourseDetail.kt | 58 +++++++------------ .../com/paw/key/data/di/RepositoryModule.kt | 8 +++ .../java/com/paw/key/data/di/ServiceModule.kt | 6 ++ .../data/remote/datasource/LikeDataSource.kt | 14 +++++ .../data/repositoryimpl/LikeRepositoryImpl.kt | 28 +++++++++ .../com/paw/key/data/service/LikeService.kt | 22 +++++++ .../key/domain/repository/LikeRepository.kt | 6 ++ 8 files changed, 116 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt create mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt create mode 100644 app/src/main/java/com/paw/key/data/service/LikeService.kt create mode 100644 app/src/main/java/com/paw/key/domain/repository/LikeRepository.kt diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt index 88e50c77..6f25735a 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt @@ -43,7 +43,8 @@ fun CourseCard( descriptionTags: List = emptyList(), // 추가 onCLickItem: () -> Unit, isShared: Boolean = false, - isRecord: Boolean = false + isRecord: Boolean = false, + onClickLike: (Boolean) -> Unit, ) { // 날짜 포맷 변환 함수 fun formatDate(dateString: String): String { @@ -148,10 +149,15 @@ fun CourseCard( contentAlignment = Alignment.Center ) { Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_heart_default), - contentDescription = null, - tint = PawKeyTheme.colors.gray400, - modifier = Modifier.size(20.dp) + imageVector = if (isLiked) + ImageVector.vectorResource(id = R.drawable.ic_heart_filled) + else + ImageVector.vectorResource(id = R.drawable.ic_heart_default), + contentDescription = "좋아요", + tint = Color.Unspecified, + modifier = Modifier + .size(24.dp) + .noRippleClickable { onClickLike(isLiked) } // 클릭 시 상태 전달 ) } } @@ -228,6 +234,7 @@ fun CourseCardPreview() { date = "2025/07/16", isShared = true, isRecord = true, + onClickLike = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt b/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt index 243bafc7..f1d19f94 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt @@ -39,7 +39,9 @@ import coil.request.ImageRequest import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.designsystem.theme.Gray100 +import com.paw.key.core.util.noRippleClickable import com.paw.key.domain.model.entity.walklist.CategoryTop3Entity +import kotlinx.serialization.json.JsonNull.content @Composable fun CourseDetail( @@ -47,15 +49,14 @@ fun CourseDetail( petName : String, date : String, location : String, - isLike : Boolean, - content : String, + onClickLike: (Boolean) -> Unit, petProfileImage : String, routeMapImageUrl : String, categorySummary : List, categoryTop3 : List, totalReviewCount : Int, walkingImageUrls : List, - + content: String, onImageClick: (String) -> Unit, modifier: Modifier = Modifier ) { @@ -109,16 +110,18 @@ fun CourseDetail( ) Icon( - imageVector = if (isLiked.value) { - ImageVector.vectorResource(id = R.drawable.ic_eye_linear_gray_valid) - } else { - ImageVector.vectorResource(id = R.drawable.ic_eye_linear_gray_invalid) - }, + imageVector = if (isLiked.value) + ImageVector.vectorResource(id = R.drawable.ic_heart_filled) + else + ImageVector.vectorResource(id = R.drawable.ic_heart_default), contentDescription = "좋아요", tint = Color.Unspecified, - modifier = Modifier.clickable { - isLiked.value = !isLiked.value - } + modifier = Modifier + .size(24.dp) + .noRippleClickable { + isLiked.value = !isLiked.value // 로컬 상태 먼저 변경 + onClickLike(!isLiked.value) // 변경된 값을 넘김 + } ) } @@ -319,35 +322,13 @@ fun CourseDetailPreview() { petName = "핑구", date = "2025/06/30", location = "홍대입구역", - isLike = true, - content = "산책로가 깨끗하고 벚꽃이 예뻐요!", + onClickLike = {}, petProfileImage = "https://pawkey-server.com/image/profile.png", routeMapImageUrl = "https://pawkey-server.com/image/map.png", categoryTop3 = listOf( - CategoryTop3Entity( - rank = 1, - optionText = "산책로가 어쩌구 저꾸", - percentage = 42, - categoryName = "산책로가 어쩌구 저꾸", - categoryOptionId = 1, - categoryId = 2 - ), - CategoryTop3Entity( - rank = 2, - optionText = "산책로가 어쩌구 저꾸", - percentage = 37, - categoryName = "산책로가 어쩌구 저꾸", - categoryOptionId = 1, - categoryId = 2 - ), - CategoryTop3Entity( - rank = 3, - optionText = "산책로가 어쩌구 저꾸", - percentage = 35, - categoryName = "산책로가 어쩌구 저꾸", - categoryOptionId = 1, - categoryId = 2 - ) + CategoryTop3Entity(rank = 1, optionText = "산책로가 어쩌구 저꾸", percentage = 42, categoryName = "", categoryOptionId = 1, categoryId = 2), + CategoryTop3Entity(rank = 2, optionText = "풍경이 예뻐요", percentage = 37, categoryName = "", categoryOptionId = 1, categoryId = 2), + CategoryTop3Entity(rank = 3, optionText = "깨끗해요", percentage = 35, categoryName = "", categoryOptionId = 1, categoryId = 2) ), totalReviewCount = 42, walkingImageUrls = listOf( @@ -355,7 +336,8 @@ fun CourseDetailPreview() { "https://pawkey-server.com/image/walk2.jpg" ), categorySummary = listOf("안전", "편리성"), - onImageClick = {}, + content = "산책로가 깨끗하고 벚꽃이 예뻐요!", + onImageClick = {} ) } } diff --git a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt index e1228286..8c166c11 100644 --- a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt +++ b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt @@ -2,6 +2,7 @@ package com.paw.key.data.di import com.paw.key.data.repositoryimpl.ArchivedListRepositoryImpl import com.paw.key.data.repositoryimpl.DummyRepositoryImpl +import com.paw.key.data.repositoryimpl.LikeRepositoryImpl import com.paw.key.data.repositoryimpl.PetProfileRepositoryImpl import com.paw.key.data.repositoryimpl.onboarding.OnboardingInfoRepositoryImpl import com.paw.key.data.repositoryimpl.onboarding.OnboardingRegionRepositoryImpl @@ -18,6 +19,7 @@ import com.paw.key.data.repositoryimpl.walklist.WalkListDetailRepositoryImpl import com.paw.key.data.repositoryimpl.walkreview.WalkReviewRepositoryImpl import com.paw.key.domain.repository.ArchivedListRepository import com.paw.key.domain.repository.DummyRepository +import com.paw.key.domain.repository.LikeRepository import com.paw.key.domain.repository.onboarding.OnboardingInfoRepository import com.paw.key.domain.repository.onboarding.OnboardingRegionRepository import com.paw.key.domain.repository.onboarding.OnboardingRepository @@ -116,6 +118,12 @@ interface RepositoryModule { impl: ArchivedListRepositoryImpl ): ArchivedListRepository + @Binds + @Singleton + fun bindLikeRepository( + impl: LikeRepositoryImpl + ): LikeRepository + @Binds @Singleton fun bindWalkReviewRepository( diff --git a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt index fff4bd8e..382a2b5d 100644 --- a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt +++ b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt @@ -2,6 +2,7 @@ package com.paw.key.data.di import com.paw.key.data.service.ArchivedListService import com.paw.key.data.service.DummyService +import com.paw.key.data.service.LikeService import com.paw.key.data.service.PetProfileService import com.paw.key.data.service.onboarding.OnboardingInfoService import com.paw.key.data.service.onboarding.OnboardingPetsService @@ -83,6 +84,11 @@ object ServiceModule { fun provideArchivedListService(retrofit: Retrofit): ArchivedListService = retrofit.create() + @Provides + @Singleton + fun provideLikeService(retrofit: Retrofit): LikeService = + retrofit.create() + @Provides @Singleton fun provideWalkReviewService(retrofit: Retrofit): WalkReviewService = diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt new file mode 100644 index 00000000..ea298d91 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt @@ -0,0 +1,14 @@ +package com.paw.key.data.remote.datasource + +import com.paw.key.data.service.LikeService +import javax.inject.Inject + +class LikeDataSource @Inject constructor( + private val likeService: LikeService +) { + suspend fun likeCourse(userId: Int, courseId: Int) = + likeService.likeCourse(userId, courseId) + + suspend fun unlikeCourse(userId: Int, courseId: Int) = + likeService.unlikeCourse(userId, courseId) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt new file mode 100644 index 00000000..b124c658 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt @@ -0,0 +1,28 @@ +package com.paw.key.data.repositoryimpl + +import com.paw.key.data.remote.datasource.LikeDataSource +import com.paw.key.domain.repository.LikeRepository +import javax.inject.Inject + +class LikeRepositoryImpl @Inject constructor( + private val dataSource: LikeDataSource +) : LikeRepository { + + override suspend fun likeCourse(userId: Int, courseId: Int): Result { + return try { + dataSource.likeCourse(userId, courseId) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun unlikeCourse(userId: Int, courseId: Int): Result { + return try { + dataSource.unlikeCourse(userId, courseId) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/LikeService.kt b/app/src/main/java/com/paw/key/data/service/LikeService.kt new file mode 100644 index 00000000..a853cd0b --- /dev/null +++ b/app/src/main/java/com/paw/key/data/service/LikeService.kt @@ -0,0 +1,22 @@ +package com.paw.key.data.service + +import com.paw.key.data.dto.response.BaseResponse +import retrofit2.http.DELETE +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Path + +interface LikeService { + + @POST("/api/v1/likes/{courseId}") + suspend fun likeCourse( + @Header("X-USER-ID") userId: Int, + @Path("courseId") courseId: Int + ): BaseResponse + + @DELETE("/api/v1/likes/{courseId}") + suspend fun unlikeCourse( + @Header("X-USER-ID") userId: Int, + @Path("courseId") courseId: Int + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/LikeRepository.kt b/app/src/main/java/com/paw/key/domain/repository/LikeRepository.kt new file mode 100644 index 00000000..b3e15304 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/repository/LikeRepository.kt @@ -0,0 +1,6 @@ +package com.paw.key.domain.repository + +interface LikeRepository { + suspend fun likeCourse(userId: Int, courseId: Int): Result + suspend fun unlikeCourse(userId: Int, courseId: Int): Result +} \ No newline at end of file From bd1ccf54d5d6162b039200f47be3afb08f926af8 Mon Sep 17 00:00:00 2001 From: JiWoo1261 Date: Wed, 16 Jul 2025 23:54:10 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat/#102=20:=20=ED=99=88=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=B0=20=EC=A0=9C=EC=99=B8=ED=95=98=EA=B3=A0=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/CourseCard.kt | 57 ++++++++----------- .../dto/response/ArchivedListResponseDto.kt | 2 +- .../entity/archivedlist/ArchivedListEntity.kt | 2 +- .../entire/tab/map/List/TabListScreen.kt | 40 ++++++------- .../map/List/viewmodel/TapListViewModel.kt | 16 ++++++ .../ui/mypage/ArchivedCourseListScreen.kt | 32 ++++++----- .../ui/mypage/SavedCourseDetailScreen.kt | 2 +- .../ui/mypage/SavedCourseListScreen.kt | 16 ++++-- .../mypage/viewmodel/ArchivedListViewModel.kt | 54 ++++++++++++++++-- .../ui/mypage/viewmodel/SavedListViewModel.kt | 11 ++++ 10 files changed, 145 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt index 3d5e6d08..1abdc3c6 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt @@ -2,6 +2,7 @@ package com.paw.key.core.designsystem.component import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -33,27 +34,21 @@ fun CourseCard( postId: Int, title: String, createdAt: String, - petName: String, isLiked: Boolean, onClickItem: () -> Unit, - modifier: Modifier = Modifier, - date: String, - representativeImageUrl: String? = null, // 추가 - petProfileImageUrl: String? = null, // 추가 - descriptionTags: List = emptyList(), // 추가 - onCLickItem: () -> Unit, - isShared: Boolean = false, - isRecord: Boolean = false, onClickLike: (Boolean) -> Unit, + petName: String, + modifier: Modifier = Modifier, + representativeImageUrl: String? = null, + petProfileImageUrl: String? = null, + descriptionTags: List = emptyList() ) { - // 날짜 포맷 변환 함수 fun formatDate(dateString: String): String { return try { - // "2025-07-15T21:27:03.54498" -> "2025/07/15" - val datePart = dateString.split("T")[0] // "2025-07-15" - datePart.replace("-", "/") // "2025/07/15" + val datePart = dateString.split("T")[0] + datePart.replace("-", "/") } catch (e: Exception) { - dateString // 실패하면 원본 반환 + dateString } } @@ -65,16 +60,12 @@ fun CourseCard( .background(Color.White, shape = RoundedCornerShape(20.dp)) .noRippleClickable { onClickItem() } ) { - // 지도 썸네일 Box( modifier = Modifier .fillMaxWidth() .aspectRatio(343f / 172f) .clip(RoundedCornerShape(10.dp)) ) { - - - // 서버 이미지 또는 기본 이미지 if (representativeImageUrl != null) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) @@ -89,7 +80,6 @@ fun CourseCard( contentScale = ContentScale.Crop ) } else { - // 기본 이미지 Image( painter = painterResource(id = R.drawable.dummy_map), contentDescription = null, @@ -101,7 +91,6 @@ fun CourseCard( ) } - // 하단 그라데이션 오버레이 Box( modifier = Modifier .fillMaxWidth() @@ -126,7 +115,6 @@ fun CourseCard( .align(Alignment.BottomStart) .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) ) { - // 반려견 프로필 이미지 if (petProfileImageUrl != null) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) @@ -140,7 +128,6 @@ fun CourseCard( contentScale = ContentScale.Crop ) } else { - // 기본 프로필 이미지 Box( modifier = Modifier .size(40.dp) @@ -158,6 +145,7 @@ fun CourseCard( } Spacer(modifier = Modifier.width(10.dp)) + Column { Text( text = title, @@ -172,33 +160,33 @@ fun CourseCard( color = Color.White ) Spacer(modifier = Modifier.width(8.dp)) - Text( - text = formatDate(date), // 포맷된 날짜 사용 + text = formatDate(createdAt), style = PawKeyTheme.typography.caption12R, color = PawKeyTheme.colors.gray100 ) } } + Spacer(modifier = Modifier.weight(1f)) + Icon( imageVector = if (isLiked) ImageVector.vectorResource(id = R.drawable.ic_heart_filled) else ImageVector.vectorResource(id = R.drawable.ic_heart_default), contentDescription = "좋아요", - tint = Color.Unspecified + tint = Color.Unspecified, + modifier = Modifier.clickable { onClickLike(!isLiked) } //클릭하면 외부에 알려줌 ) } } Spacer(modifier = Modifier.height(12.dp)) - // 서버에서 받은 태그들 사용 ChipRow( tags = descriptionTags, - modifier = Modifier - .padding(start = 16.dp, end = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp) ) Spacer(modifier = Modifier.height(12.dp)) @@ -206,7 +194,7 @@ fun CourseCard( HorizontalDivider( color = PawKeyTheme.colors.gray50, thickness = 1.dp, - modifier = Modifier.padding(start = 16.dp, end = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp) ) } } @@ -218,13 +206,14 @@ fun CourseCardPreview() { CourseCard( postId = 1, title = "홍대 주변 좋은 산책 코스", - createdAt = "2025/07/16", - representativeImageUrl = "https://pawkey-server.com/image.jpg", + createdAt = "2025-07-16T21:27:03.54498", + isLiked = true, + onClickItem = {}, + onClickLike = {}, petName = "후추", + representativeImageUrl = "https://pawkey-server.com/image.jpg", petProfileImageUrl = "https://pawkey-server.com/profile.jpg", - descriptionTags = listOf("이륜차 거의 없음", "물그릇 비치", "쉴 곳 있음"), - isLiked = true, - onClickLike = {} + descriptionTags = listOf("이륜차 거의 없음", "물그릇 비치", "쉴 곳 있음") ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt index 516c1a24..9e3f4bf9 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt @@ -36,7 +36,7 @@ data class ArchivedDto( val descriptionTags: List ) { fun toEntity() = ArchivedListEntity( - postId = postId.toLong(), + postId = postId, createdAt = createdAt, isLiked = isLike, title = title, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/archivedlist/ArchivedListEntity.kt b/app/src/main/java/com/paw/key/domain/model/entity/archivedlist/ArchivedListEntity.kt index c8c587e2..7b7db616 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/archivedlist/ArchivedListEntity.kt +++ b/app/src/main/java/com/paw/key/domain/model/entity/archivedlist/ArchivedListEntity.kt @@ -5,7 +5,7 @@ data class ArchivedListPostsEntity( ) data class ArchivedListEntity( - val postId: Long, + val postId: Int, val createdAt: String, val isLiked: Boolean, val title: String, diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt index 758a862e..da48ff4f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt @@ -3,24 +3,14 @@ package com.paw.key.presentation.ui.course.entire.tab.map.List import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -41,7 +31,8 @@ import com.paw.key.presentation.ui.course.entire.tab.map.List.viewmodel.TapListV private fun PreviewTabListScreen() { PawKeyTheme { TabListScreen( - navigateToDetail = {} + navigateToDetail = {}, + onClickLike = { _, _ -> } ) } } @@ -55,7 +46,10 @@ fun TapListRoute( TabListScreen( modifier = modifier, navigateToDetail = navigateToDetail, - viewModel = viewModel + viewModel = viewModel, + onClickLike = { postId, isLiked -> + viewModel.toggleLike(postId = postId, isLiked = isLiked) + } ) } @@ -64,13 +58,13 @@ fun TabListScreen( navigateToDetail: () -> Unit, modifier: Modifier = Modifier, viewModel: TapListViewModel = hiltViewModel(), + onClickLike: (postId: Int, isLiked: Boolean) -> Unit ) { var showBottomSheet by remember { mutableStateOf(false) } val listState by viewModel.state.collectAsStateWithLifecycle() Column( - modifier = modifier - .fillMaxSize() + modifier = modifier.fillMaxSize() ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -84,10 +78,9 @@ fun TabListScreen( imageVector = ImageVector.vectorResource(R.drawable.ic_course_optin_filter), contentDescription = "filter", tint = Color.Unspecified, - modifier = Modifier - .noRippleClickable { - showBottomSheet = true - } + modifier = Modifier.noRippleClickable { + showBottomSheet = true + } ) OptionChip( text = if (viewModel.isFilterApplied()) "필터 적용됨" else "선택한 옵션이 없어요", @@ -101,10 +94,6 @@ fun TabListScreen( .background(PawKeyTheme.colors.white2) .padding(bottom = 36.dp) ) { - - // Todo : 나중에 서버용 리스트로 변경 - - // 로딩 상태 표시 if (listState.isLoading) { item { Box( @@ -132,6 +121,9 @@ fun TabListScreen( petProfileImageUrl = post.writer.petProfileImageUrl, descriptionTags = post.descriptionTags, isLiked = post.isLike, + onClickLike = { isLiked -> + onClickLike(post.postId, isLiked) + }, onClickItem = { navigateToDetail() } ) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt index 385434c4..1b198763 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt @@ -282,4 +282,20 @@ class TapListViewModel @Inject constructor( fun isFilterApplied(): Boolean { return isAllOptionsSelected() } + + fun toggleLike(postId: Int, isLiked: Boolean) { + viewModelScope.launch { + _state.update { state -> + val updatedPosts = state.postsResult?.posts?.map { + if (it.postId == postId) it.copy(isLike = isLiked) else it + } ?: emptyList() + + val updatedPostsResult = state.postsResult?.copy(posts = updatedPosts) + + state.copy( + postsResult = updatedPostsResult + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt index f4b16637..71d1cc0b 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -14,38 +13,40 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.core.designsystem.component.CourseCard import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.mypage.state.SavedListState -import com.paw.key.presentation.ui.mypage.viewmodel.SavedListViewModel +import com.paw.key.presentation.ui.mypage.state.ArchivedListState +import com.paw.key.presentation.ui.mypage.viewmodel.ArchivedListViewModel @Composable fun ArchivedCourseRoute( navigateUp: () -> Unit, navigateNext: () -> Unit, modifier: Modifier = Modifier, - viewModel: SavedListViewModel = hiltViewModel() + viewModel: ArchivedListViewModel = hiltViewModel() ) { val state = viewModel.state.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - viewModel.getSavedList(2) - } + ArchivedCourseListScreen( state = state.value, navigateUp = navigateUp, navigateNext = navigateNext, + onClickLike = { postId, isLiked -> + viewModel.toggleLike(postId = postId, isLiked = isLiked) + }, modifier = modifier ) } @Composable fun ArchivedCourseListScreen( - state: SavedListState, + state: ArchivedListState, navigateUp: () -> Unit, navigateNext: () -> Unit, + onClickLike: (postId: Int, isLiked: Boolean) -> Unit, modifier: Modifier = Modifier ) { Column { TopBar( - title = "저장한 산책 루트", + title = "내가 기록한 산책 루트", onBackClick = navigateUp ) @@ -55,9 +56,7 @@ fun ArchivedCourseListScreen( .padding(16.dp) .background(PawKeyTheme.colors.white1) ) { - itemsIndexed( - items = state.courseList - ) { _, item -> + itemsIndexed(state.courseList) { _, item -> CourseCard( postId = item.postId.toInt(), title = item.title, @@ -67,7 +66,8 @@ fun ArchivedCourseListScreen( petProfileImageUrl = item.writer.first().petProfileImageUrl, descriptionTags = item.descriptionTags, isLiked = item.isLiked, - onClickItem = navigateNext + onClickItem = navigateNext, + onClickLike = { isLiked -> onClickLike(item.postId.toInt(), isLiked) }, ) } } @@ -78,9 +78,11 @@ fun ArchivedCourseListScreen( @Composable fun ArchivedCourseListScreenPreview() { PawKeyTheme { - SavedCourseListScreen(state = SavedListState(), + ArchivedCourseListScreen( + state = ArchivedListState(), navigateUp = {}, - navigateNext = {} + navigateNext = {}, + onClickLike = { _, _ -> } ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseDetailScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseDetailScreen.kt index a24a5b81..12c2127b 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseDetailScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseDetailScreen.kt @@ -101,7 +101,7 @@ fun SavedCourseDetailScreen( petName = petName, date = date, location = location, - isLike = isLike, + onClickLike = {}, content = content, petProfileImage = petProfileImage, routeMapImageUrl = routeMapImageUrl, diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt index d92c3f2f..82aebbd4 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt @@ -29,6 +29,9 @@ fun SavedCourseRoute( state = state.value, navigateUp = navigateUp, navigateNext = navigateNext, + onClickLike = { postId, isLiked -> + viewModel.toggleLike(postId = postId, isLiked = isLiked) + }, modifier = modifier ) } @@ -38,6 +41,7 @@ fun SavedCourseListScreen( state: SavedListState, navigateUp: () -> Unit, navigateNext: () -> Unit, + onClickLike: (postId: Int, isLiked: Boolean) -> Unit, modifier: Modifier = Modifier ) { Column { @@ -52,9 +56,7 @@ fun SavedCourseListScreen( .padding(16.dp) .background(PawKeyTheme.colors.white1) ) { - itemsIndexed( - items = state.courseList - ) { _, item -> + itemsIndexed(state.courseList) { _, item -> CourseCard( postId = item.postId.toInt(), title = item.title, @@ -65,7 +67,7 @@ fun SavedCourseListScreen( descriptionTags = item.descriptionTags, isLiked = item.isLiked, onClickItem = navigateNext, - isRecord = true + onClickLike = { isLiked -> onClickLike(item.postId.toInt(), isLiked) }, ) } } @@ -76,9 +78,11 @@ fun SavedCourseListScreen( @Composable fun SavedCourseListScreenPreview() { PawKeyTheme { - SavedCourseListScreen(state = SavedListState(), + SavedCourseListScreen( + state = SavedListState(), navigateUp = {}, - navigateNext = {} + navigateNext = {}, + onClickLike = { _, _ -> } ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/ArchivedListViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/ArchivedListViewModel.kt index 84182c04..f19e3f1d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/ArchivedListViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/ArchivedListViewModel.kt @@ -1,18 +1,62 @@ package com.paw.key.presentation.ui.mypage.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel -import com.paw.key.presentation.ui.mypage.state.ArchivedDetailState +import androidx.lifecycle.viewModelScope +import com.paw.key.core.designsystem.component.CourseCard +import com.paw.key.domain.repository.ArchivedListRepository +import com.paw.key.domain.repository.SavedListRepository +import com.paw.key.presentation.ui.mypage.state.ArchivedListSideEffect import com.paw.key.presentation.ui.mypage.state.ArchivedListState -import com.paw.key.presentation.ui.mypage.state.SavedDetailState +import com.paw.key.presentation.ui.mypage.state.MyPageSideEffect +import com.paw.key.presentation.ui.mypage.state.PetProfileSideEffect.NavigateNext +import com.paw.key.presentation.ui.mypage.state.SavedListSideEffect +import com.paw.key.presentation.ui.mypage.state.SavedListState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class ArchivedListViewModel @Inject constructor() : ViewModel() { +class ArchivedListViewModel @Inject constructor( + private val archivedListRepository: ArchivedListRepository +) : ViewModel() { + private val _state = MutableStateFlow(ArchivedListState()) - val state: StateFlow - get() = _state.asStateFlow() + val state: StateFlow = _state.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect: MutableSharedFlow = _sideEffect + + fun getArchivedList(userId: Int) { + viewModelScope.launch { + archivedListRepository.getArchivedList(userId) + .onSuccess { result -> + _state.update { + it.copy( + courseList = result.posts + ) + } + } + .onFailure { e -> + Log.e("ArchivedListViewModel", "저장한 게시물 불러오기 실패", e) + _sideEffect.emit(ArchivedListSideEffect.ShowSnackBar(e.message ?: "알 수 없는 오류")) + } + } + } + fun toggleLike(postId: Int, isLiked: Boolean) { + viewModelScope.launch { + _state.update { state -> + state.copy( + courseList = state.courseList.map { + if (it.postId == postId) it.copy(isLiked = isLiked) else it + } + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/SavedListViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/SavedListViewModel.kt index 5fa12054..256257b9 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/SavedListViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/viewmodel/SavedListViewModel.kt @@ -46,4 +46,15 @@ class SavedListViewModel @Inject constructor( } } } + fun toggleLike(postId: Int, isLiked: Boolean) { + viewModelScope.launch { + _state.update { state -> + state.copy( + courseList = state.courseList.map { + if (it.postId == postId) it.copy(isLiked = isLiked) else it + } + ) + } + } + } } \ No newline at end of file