diff --git a/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt b/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt index 36ed472..dcf1cd2 100644 --- a/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt +++ b/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt @@ -15,6 +15,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @@ -25,6 +26,9 @@ object NetworkModule { @Singleton fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient { return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) .addInterceptor(authInterceptor) .addInterceptor( HttpLoggingInterceptor().apply { diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityDetailResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityDetailResponseDto.kt new file mode 100644 index 0000000..e267d14 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/community/CommunityDetailResponseDto.kt @@ -0,0 +1,38 @@ +package com.hsLink.hslink.data.dto.response.community + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CommunityDetailResponseDto( + @SerialName("id") + val id: Int, + @SerialName("author") + val author: String, + @SerialName("studentId") + val studentId: String, + @SerialName("authorStatus") + val authorStatus: String, + @SerialName("title") + val title: String, + @SerialName("body") + val body: String, + @SerialName("mine") + val mine: Boolean, + @SerialName("comments") + val comments: List, +) + +@Serializable +data class CommentDto( + @SerialName("id") + val id: Int, + @SerialName("commenter") + val commenter: String, + @SerialName("commenterStatus") + val commenterStatus: String, + @SerialName("content") + val content: String, + @SerialName("mine") + val mine: Boolean, +) diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt index e6a7f1e..b43bf25 100644 --- a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CommunityRepositoryImpl.kt @@ -4,6 +4,7 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityDetailResponseDto import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto import com.hsLink.hslink.data.paging.CommunityPagingSource import com.hsLink.hslink.data.service.commuunity.CommunityPostService @@ -31,4 +32,14 @@ class CommunityRepositoryImpl @Inject constructor( pagingSourceFactory = { CommunityPagingSource(communityPostService, type) } ).flow } + + override suspend fun getCommunityDetail(postId: Int): Result = + runCatching { + val response = communityPostService.getCommunityDetail(postId) + if (response.isSuccess) { + response.result + } else { + throw Exception(response.message) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt b/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt index 2068cfb..3c48be0 100644 --- a/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt +++ b/app/src/main/java/com/hsLink/hslink/data/service/commuunity/CommunityPostService.kt @@ -2,11 +2,13 @@ package com.hsLink.hslink.data.service.commuunity import com.hsLink.hslink.core.network.BaseResponse import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityDetailResponseDto import com.hsLink.hslink.data.dto.response.community.CommunityListResponseDto import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query interface CommunityPostService { @@ -20,4 +22,9 @@ interface CommunityPostService { @Query("type") type: String, @Query("page") page: Int, ): BaseResponse + + @GET("posts/{postId}") + suspend fun getCommunityDetail( + @Path("postId") postId: Int, + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt index 48addc9..3071e67 100644 --- a/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/community/CommunityRepository.kt @@ -2,6 +2,7 @@ package com.hsLink.hslink.domain.repository.community import androidx.paging.PagingData import com.hsLink.hslink.data.dto.request.community.PostRequestDto +import com.hsLink.hslink.data.dto.response.community.CommunityDetailResponseDto import com.hsLink.hslink.data.dto.response.community.CommunityPostResponseDto import com.hsLink.hslink.domain.model.community.CommunityPost import kotlinx.coroutines.flow.Flow @@ -10,4 +11,6 @@ interface CommunityRepository { suspend fun createCommunityPost(communityRequestDto: PostRequestDto): Result fun getCommunityPosts(type: String): Flow> + + suspend fun getCommunityDetail(postId: Int): Result } diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/post/CommunityPostNavigation.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/post/CommunityPostNavigation.kt index 86e1417..f1b85ee 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/post/CommunityPostNavigation.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/navigation/post/CommunityPostNavigation.kt @@ -10,7 +10,7 @@ import com.hsLink.hslink.presentation.community.screen.post.CommunityPostRoute import kotlinx.serialization.Serializable fun NavController.navigateToCommunityPost( - postId: String, + postId: Int, navOptions: NavOptions? = null ) { navigate(CommunityPost(postId), navOptions) @@ -20,8 +20,12 @@ fun NavGraphBuilder.communityPostNavGraph( padding: PaddingValues, navigateUp: () -> Unit, ) { - composable { + composable { backStackEntry -> + + val postId = backStackEntry.arguments?.getInt("postId") ?: -1 + CommunityPostRoute( + postId = postId, paddingValues = padding, navigateUp = navigateUp ) @@ -29,4 +33,4 @@ fun NavGraphBuilder.communityPostNavGraph( } @Serializable -data class CommunityPost(val postId: String) : Route \ No newline at end of file +data class CommunityPost(val postId: Int) : Route \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/screen/post/CommunityPostScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/screen/post/CommunityPostScreen.kt index 9c7dcee..1b6e992 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/screen/post/CommunityPostScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/screen/post/CommunityPostScreen.kt @@ -9,27 +9,33 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.hsLink.hslink.R import com.hsLink.hslink.core.designsystem.component.HsLinkDialog import com.hsLink.hslink.core.designsystem.component.HsLinkTopBar import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme -import com.hsLink.hslink.data.local.Comment -import com.hsLink.hslink.data.local.PostDetail import com.hsLink.hslink.presentation.community.component.CommentInput import com.hsLink.hslink.presentation.community.component.CommentItem import com.hsLink.hslink.presentation.community.component.EmptyComment import com.hsLink.hslink.presentation.community.component.PostContent import com.hsLink.hslink.presentation.community.component.PostHeader +import com.hsLink.hslink.presentation.community.state.CommunityDetailState +import com.hsLink.hslink.presentation.community.viewmodel.CommunityViewModel @Preview(showBackground = true) @@ -37,63 +43,48 @@ import com.hsLink.hslink.presentation.community.component.PostHeader private fun CommunityPostScreenPreview() { HsLinkTheme { CommunityPostScreen( + postId = 1, paddingValues = PaddingValues(), - navigateUp = {} + navigateUp = {}, + postDetailState = CommunityDetailState.Loading, ) } } @Composable fun CommunityPostRoute( + postId: Int, paddingValues: PaddingValues, navigateUp: () -> Unit, + viewModel: CommunityViewModel = hiltViewModel(), ) { + + LaunchedEffect(postId) { + viewModel.getPostDetail(postId) + } + + val postDetailState by viewModel.postDetailState.collectAsState() + CommunityPostScreen( + postId = postId, paddingValues = paddingValues, - navigateUp = navigateUp + navigateUp = navigateUp, + postDetailState = postDetailState, ) } @Composable fun CommunityPostScreen( + postId: Int, paddingValues: PaddingValues, navigateUp: () -> Unit, + postDetailState: CommunityDetailState, modifier: Modifier = Modifier, ) { - - val postDetail = remember { - PostDetail( - id = "1", - authorName = "송효재", - authorMajor = "재직중", - boardType = "자유게시판", - timeAgo = "21학번", - title = "추천 채용 한성 IT 추천 채용 공고 - 네이버 영업직 구합니다.", - content = "제가 다니고 있는 한성 it에서 추천 채용이 올라와 공유드립니다. 이미지 첨고해주세요", - isMyPost = true, - comments = listOf( - Comment( - id = "1", - authorName = "송효재", - timeAgo = "21학번", - content = "자기소개서 어떻게 작성하셨나요? 주로 보는 인재상이 있는지 궁금합니다.", - isMyComment = true - ), - Comment( - id = "2", - authorName = "김철수", - timeAgo = "20학번", - content = "좋은 정보 감사합니다!", - isMyComment = false - ) - ) - ) - } - var commentText by remember { mutableStateOf("") } var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteCommentDialog by remember { mutableStateOf(false) } - var selectedCommentId by remember { mutableStateOf(null) } + var selectedCommentId by remember { mutableStateOf(null) } Box( modifier = modifier @@ -116,9 +107,13 @@ fun CommunityPostScreen( }, leftIcon = R.drawable.ic_community_post_leftarrow, onLeftIconClick = navigateUp, - rightIconSecond = if (postDetail.isMyPost) R.drawable.ic_community_kebab else null, + rightIconSecond = if (postDetailState is CommunityDetailState.Success && postDetailState.post.mine) { + R.drawable.ic_community_kebab + } else { + null + }, onRightIconSecondClick = { - if (postDetail.isMyPost) { + if (postDetailState is CommunityDetailState.Success && postDetailState.post.mine) { showDeleteDialog = true } } @@ -129,74 +124,106 @@ fun CommunityPostScreen( color = HsLinkTheme.colors.Grey100 ) - LazyColumn( - modifier = Modifier - .weight(1f) - .imePadding() - ) { - item { - PostHeader( - authorName = postDetail.authorName, - authorMajor = postDetail.authorMajor, - boardType = postDetail.boardType, - timeAgo = postDetail.timeAgo - ) + when (postDetailState) { + is CommunityDetailState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .weight(1f), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } } - item { - PostContent( - title = postDetail.title, - content = postDetail.content - ) + is CommunityDetailState.Error -> { + Box( + modifier = Modifier + .fillMaxSize() + .weight(1f), + contentAlignment = Alignment.Center + ) { + Text( + text = postDetailState.message, + color = Color.Red, + style = HsLinkTheme.typography.body_16Normal + ) + } } - item { - HorizontalDivider( - thickness = 8.dp, - color = HsLinkTheme.colors.Grey100 - ) - } + is CommunityDetailState.Success -> { + val postDetail = postDetailState.post - if (postDetail.comments.isEmpty()) { - item { - EmptyComment() - } - } else { - items( - items = postDetail.comments, - key = { it.id } - ) { comment -> - CommentItem( - authorName = comment.authorName, - timeAgo = comment.timeAgo, - content = comment.content, - isMyComment = comment.isMyComment, - onDeleteClick = { - selectedCommentId = comment.id - showDeleteCommentDialog = true + LazyColumn( + modifier = Modifier + .weight(1f) + .imePadding() + ) { + item { + PostHeader( + authorName = postDetail.author, + authorMajor = postDetail.authorStatus, + boardType = "자유게시판", + timeAgo = postDetail.studentId, + ) + } + + item { + PostContent( + title = postDetail.title, + content = postDetail.body + ) + } + + item { + HorizontalDivider( + thickness = 8.dp, + color = HsLinkTheme.colors.Grey100 + ) + } + + if (postDetail.comments.isEmpty()) { + item { + EmptyComment() } - ) + } else { + items( + items = postDetail.comments, + key = { it.id } + ) { comment -> + CommentItem( + authorName = comment.commenter, + timeAgo = comment.commenterStatus, + content = comment.content, + isMyComment = comment.mine, + onDeleteClick = { + selectedCommentId = comment.id + showDeleteCommentDialog = true + } + ) - HorizontalDivider( - thickness = 1.dp, - color = HsLinkTheme.colors.Grey100 - ) + HorizontalDivider( + thickness = 1.dp, + color = HsLinkTheme.colors.Grey100 + ) + } + } } - } - } - CommentInput( - value = commentText, - onValueChange = { commentText = it }, - onSendClick = { - // TODO: 댓글 전송 로직 - commentText = "" + CommentInput( + value = commentText, + onValueChange = { commentText = it }, + onSendClick = { + // TODO: 댓글 전송 로직 (postId 사용) + commentText = "" + } + ) } - ) + } } } - if (showDeleteDialog) { + if (showDeleteDialog && postDetailState is CommunityDetailState.Success && postDetailState.post.mine) { HsLinkDialog( title = "게시글을 삭제하시겠습니까?", message = "삭제된 게시글은 복구할 수 없습니다.", @@ -204,7 +231,7 @@ fun CommunityPostScreen( dismissText = "취소", onConfirm = { showDeleteDialog = false - // TODO: 게시글 삭제 로직 + // TODO: 게시글 삭제 로직 (postId 사용) navigateUp() }, onDismiss = { @@ -213,7 +240,7 @@ fun CommunityPostScreen( ) } - if (showDeleteCommentDialog) { + if (showDeleteCommentDialog && selectedCommentId != null) { HsLinkDialog( title = "댓글을 삭제하시겠습니까?", message = "삭제된 댓글은 복구할 수 없습니다.", @@ -222,6 +249,7 @@ fun CommunityPostScreen( onConfirm = { showDeleteCommentDialog = false // TODO: 댓글 삭제 로직 (selectedCommentId 사용) + selectedCommentId = null }, onDismiss = { showDeleteCommentDialog = false @@ -229,4 +257,4 @@ fun CommunityPostScreen( } ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/state/CommunityContract.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/state/CommunityContract.kt index e24acd1..4459951 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/state/CommunityContract.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/state/CommunityContract.kt @@ -1,6 +1,7 @@ package com.hsLink.hslink.presentation.community.state import androidx.compose.runtime.Immutable +import com.hsLink.hslink.data.dto.response.community.CommunityDetailResponseDto import com.hsLink.hslink.domain.model.community.CommunityPostResponseEntity @Immutable @@ -8,4 +9,11 @@ data class CommunityContract ( val isLoading: Boolean = false, val error: String? = null, val communityEntity: CommunityPostResponseEntity ? = null -) \ No newline at end of file +) + + +sealed interface CommunityDetailState { + data object Loading : CommunityDetailState + data class Success(val post: CommunityDetailResponseDto) : CommunityDetailState + data class Error(val message: String) : CommunityDetailState +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt b/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt index b8a2d2f..aebb5d3 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/community/viewmodel/CommunityViewModel.kt @@ -8,9 +8,11 @@ import com.hsLink.hslink.data.dto.request.community.PostRequestDto import com.hsLink.hslink.domain.model.community.CommunityPost import com.hsLink.hslink.domain.repository.community.CommunityRepository import com.hsLink.hslink.presentation.community.component.CommunityTab +import com.hsLink.hslink.presentation.community.state.CommunityDetailState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -23,6 +25,9 @@ class CommunityViewModel @Inject constructor( private val _selectedTab = MutableStateFlow(CommunityTab.Popular) val selectedTab: Flow = _selectedTab + private val _postDetailState = MutableStateFlow(CommunityDetailState.Loading) + val postDetailState: StateFlow = _postDetailState + val communityPosts: Flow> = _selectedTab .flatMapLatest { tab -> repository.getCommunityPosts(tab.name.lowercase()) @@ -49,4 +54,17 @@ class CommunityViewModel @Inject constructor( } } } + + fun getPostDetail(postId: Int) { + viewModelScope.launch { + _postDetailState.value = CommunityDetailState.Loading + repository.getCommunityDetail(postId) + .onSuccess { response -> + _postDetailState.value = CommunityDetailState.Success(response) + } + .onFailure { error -> + _postDetailState.value = CommunityDetailState.Error(error.message ?: "게시물 상세 조회 실패") + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt index 39122cd..56f84b6 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt @@ -51,7 +51,7 @@ fun MainNavHost( communityNavGraph( padding = padding, navigateToWriteCommunity = navigator::navigateWriteCommunity, - navigateToPost = { postId -> navigator.navigateToCommunityPost(postId.toString()) }, + navigateToPost = { postId -> navigator.navigateToCommunityPost(postId) }, ) mypageNavGraph( diff --git a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt index 6f88b50..613ec9f 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt @@ -65,7 +65,7 @@ class MainNavigator( navController.navigateToCommunity(navOptions) } - fun navigateToCommunityPost(postId: String, navOptions: NavOptions? = null) { + fun navigateToCommunityPost(postId: Int, navOptions: NavOptions? = null) { navController.navigateToCommunityPost(postId, navOptions) }