Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package kr.ac.anu.mumu.data.datasource

import kr.ac.anu.mumu.data.model.BaseResponse
import kr.ac.anu.mumu.data.model.BookmarkDto
import kr.ac.anu.mumu.data.model.CommentDto
import kr.ac.anu.mumu.data.model.CommentRequestDto
import kr.ac.anu.mumu.data.model.CommunityPostDto
import kr.ac.anu.mumu.data.model.CommunityRequestDto
import kr.ac.anu.mumu.data.model.LikeDto
import kr.ac.anu.mumu.data.model.PaginatedData
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query

interface CommunityService {
Expand All @@ -13,4 +21,38 @@ interface CommunityService {
@Query("page") page: Int = 0,
@Query("size") size: Int = 50
): Response<BaseResponse<PaginatedData<CommunityPostDto>>>

@GET("/api/community/best")
suspend fun getBestPosts(): Response<BaseResponse<List<CommunityPostDto>>>

@GET("/api/community/{communityId}")
suspend fun getPost(
@Path("communityId") communityId: Long
): Response<BaseResponse<CommunityPostDto>>

@POST("/api/community")
suspend fun createPost(
@Body request: CommunityRequestDto
): Response<BaseResponse<CommunityPostDto>>

@POST("/api/likes/community/{communityId}")
suspend fun toggleLike(
@Path("communityId") communityId: Long
): Response<BaseResponse<LikeDto>>

@POST("/api/bookmarks/{communityId}")
suspend fun toggleBookmark(
@Path("communityId") communityId: Long
): Response<BaseResponse<BookmarkDto>>

@GET("/api/community/{communityId}/comments")
suspend fun getComments(
@Path("communityId") communityId: Long
): Response<BaseResponse<List<CommentDto>>>

@POST("/api/community/{communityId}/comments")
suspend fun createComment(
@Path("communityId") communityId: Long,
@Body request: CommentRequestDto
): Response<BaseResponse<CommentDto>>
}
34 changes: 34 additions & 0 deletions app/src/main/java/kr/ac/anu/mumu/data/model/CommunityPostDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,37 @@ data class CommunityPostDto(
val updatedAt: String,
val thumbnailUrl: String?
)

data class CommunityRequestDto(
val category: String,
val title: String,
val content: String,
val hashtags: List<String>,
val petId: Long? = null
)

data class CommentDto(
val commentId: Long,
val communityId: Long,
val userId: Long,
val parentCommentId: Long?,
val content: String,
val likeCount: Int,
val createdAt: String,
val updatedAt: String
)

data class CommentRequestDto(
val content: String,
val parentCommentId: Long? = null
)

data class LikeDto(
val liked: Boolean,
val likeCount: Int
)

data class BookmarkDto(
val bookmarked: Boolean,
val bookmarkCount: Int
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package kr.ac.anu.mumu.data.repository

import kr.ac.anu.mumu.data.datasource.CommunityService
import kr.ac.anu.mumu.data.model.CommentDto
import kr.ac.anu.mumu.data.model.CommentRequestDto
import kr.ac.anu.mumu.data.model.CommunityPostDto
import kr.ac.anu.mumu.data.model.CommunityRequestDto
import kr.ac.anu.mumu.domain.model.CommunityComment
import kr.ac.anu.mumu.domain.model.CommunityPost
import kr.ac.anu.mumu.domain.repository.CommunityRepository
import javax.inject.Inject
Expand All @@ -15,12 +20,90 @@ class CommunityRepositoryImpl @Inject constructor(
if (!response.isSuccessful || body?.success != true) {
error(body?.message ?: "게시글을 불러오지 못했습니다. (${response.code()})")
}
body.data?.content.orEmpty().map { post ->
CommunityPost(
id = post.communityId,
title = post.title,
thumbnailUrl = post.thumbnailUrl
)
body.data?.content.orEmpty().map { it.toDomain() }
}

override suspend fun getBestPosts(): Result<List<CommunityPost>> = runCatching {
val response = communityService.getBestPosts()
val body = response.body()
if (!response.isSuccessful || body?.success != true) {
error(body?.message ?: "인기 게시글을 불러오지 못했습니다. (${response.code()})")
}
body.data.orEmpty().map { it.toDomain() }
}

override suspend fun getPost(postId: Long): Result<CommunityPost> = runCatching {
val response = communityService.getPost(postId)
response.requireData("게시글을 불러오지 못했습니다.").toDomain()
}

override suspend fun createPost(
category: String,
title: String,
content: String,
hashtags: List<String>
): Result<CommunityPost> = runCatching {
val response = communityService.createPost(
CommunityRequestDto(category, title, content, hashtags)
)
response.requireData("게시글을 등록하지 못했습니다.").toDomain()
}

override suspend fun toggleLike(postId: Long): Result<Pair<Boolean, Int>> = runCatching {
val data = communityService.toggleLike(postId).requireData("좋아요 처리에 실패했습니다.")
data.liked to data.likeCount
}

override suspend fun toggleBookmark(postId: Long): Result<Pair<Boolean, Int>> = runCatching {
val data = communityService.toggleBookmark(postId).requireData("북마크 처리에 실패했습니다.")
data.bookmarked to data.bookmarkCount
}

override suspend fun getComments(postId: Long): Result<List<CommunityComment>> = runCatching {
val response = communityService.getComments(postId)
val body = response.body()
if (!response.isSuccessful || body?.success != true) {
error(body?.message ?: "댓글을 불러오지 못했습니다. (${response.code()})")
}
body.data.orEmpty().map { it.toDomain() }
}

override suspend fun createComment(postId: Long, content: String): Result<CommunityComment> = runCatching {
communityService.createComment(postId, CommentRequestDto(content))
.requireData("댓글을 등록하지 못했습니다.")
.toDomain()
}

private fun CommunityPostDto.toDomain() = CommunityPost(
id = communityId,
userId = userId,
title = title,
content = content,
category = category,
hashtags = hashtags.orEmpty(),
thumbnailUrl = thumbnailUrl,
viewCount = viewCount,
likeCount = likeCount,
commentCount = commentCount,
bookmarkCount = bookmarkCount,
createdAt = createdAt
)

private fun CommentDto.toDomain() = CommunityComment(
id = commentId,
userId = userId,
content = content,
likeCount = likeCount,
createdAt = createdAt
)

private fun <T> retrofit2.Response<kr.ac.anu.mumu.data.model.BaseResponse<T>>.requireData(
fallbackMessage: String
): T {
val body = body()
if (!isSuccessful || body?.success != true) {
error(body?.message ?: "$fallbackMessage (${code()})")
}
return body.data ?: error("서버 응답이 비어 있습니다.")
}
}
19 changes: 18 additions & 1 deletion app/src/main/java/kr/ac/anu/mumu/domain/model/CommunityPost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@ package kr.ac.anu.mumu.domain.model

data class CommunityPost(
val id: Long,
val userId: Long,
val title: String,
val thumbnailUrl: String?
val content: String,
val category: String,
val hashtags: List<String>,
val thumbnailUrl: String?,
val viewCount: Int,
val likeCount: Int,
val commentCount: Int,
val bookmarkCount: Int,
val createdAt: String
)

data class CommunityComment(
val id: Long,
val userId: Long,
val content: String,
val likeCount: Int,
val createdAt: String
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
package kr.ac.anu.mumu.domain.repository

import kr.ac.anu.mumu.domain.model.CommunityComment
import kr.ac.anu.mumu.domain.model.CommunityPost

interface CommunityRepository {
suspend fun getPosts(): Result<List<CommunityPost>>
suspend fun getBestPosts(): Result<List<CommunityPost>>
suspend fun getPost(postId: Long): Result<CommunityPost>
suspend fun createPost(
category: String,
title: String,
content: String,
hashtags: List<String>
): Result<CommunityPost>
suspend fun toggleLike(postId: Long): Result<Pair<Boolean, Int>>
suspend fun toggleBookmark(postId: Long): Result<Pair<Boolean, Int>>
suspend fun getComments(postId: Long): Result<List<CommunityComment>>
suspend fun createComment(postId: Long, content: String): Result<CommunityComment>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package kr.ac.anu.mumu.presentation.main

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import coil3.load
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import kr.ac.anu.mumu.databinding.FragmentCommunityDetailBinding
import kr.ac.anu.mumu.presentation.main.adapter.CommunityCommentAdapter

@AndroidEntryPoint
class CommunityDetailFragment : Fragment() {

private var _binding: FragmentCommunityDetailBinding? = null
private val binding get() = _binding!!
private val viewModel: CommunityDetailViewModel by viewModels()
private val commentAdapter = CommunityCommentAdapter()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCommunityDetailBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding.rvComments.apply {
adapter = commentAdapter
layoutManager = LinearLayoutManager(requireContext())
isNestedScrollingEnabled = false
}
binding.btnRetry.setOnClickListener { viewModel.load() }
binding.btnLike.setOnClickListener { viewModel.toggleLike() }
binding.btnBookmark.setOnClickListener { viewModel.toggleBookmark() }
binding.btnComment.setOnClickListener {
val content = binding.etComment.text?.toString().orEmpty()
if (content.isNotBlank()) {
viewModel.addComment(content)
binding.etComment.text?.clear()
}
}

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect(::render)
}
}
}

private fun render(state: CommunityDetailUiState) {
binding.progressDetail.visibility = if (state is CommunityDetailUiState.Loading) View.VISIBLE else View.GONE
binding.layoutError.visibility = if (state is CommunityDetailUiState.Error) View.VISIBLE else View.GONE
binding.scrollContent.visibility = if (state is CommunityDetailUiState.Success) View.VISIBLE else View.GONE

when (state) {
CommunityDetailUiState.Loading -> Unit
is CommunityDetailUiState.Error -> binding.tvError.text = state.message
is CommunityDetailUiState.Success -> {
val post = state.post
binding.tvPostAuthor.text = "사용자 ${post.userId}"
binding.tvCategory.text = post.category.toCategoryLabel()
binding.tvTitle.text = post.title
binding.tvContent.text = post.content
binding.tvMeta.text =
"${post.createdAt.take(10).replace('-', '.')} · 조회 ${post.viewCount} · 댓글 ${post.commentCount}"
binding.tvHashtags.text = post.hashtags.joinToString(" ") { "#$it" }
binding.tvHashtags.visibility = if (post.hashtags.isEmpty()) View.GONE else View.VISIBLE
binding.btnLike.text = if (state.liked) "♥ ${post.likeCount}" else "♡ ${post.likeCount}"
binding.btnBookmark.text = if (state.bookmarked) "북마크됨" else "북마크 ${post.bookmarkCount}"
if (post.thumbnailUrl.isNullOrBlank()) {
binding.ivThumbnail.visibility = View.GONE
} else {
binding.ivThumbnail.visibility = View.VISIBLE
binding.ivThumbnail.load(post.thumbnailUrl)
}
commentAdapter.submitList(state.comments)
binding.tvCommentEmpty.visibility = if (state.comments.isEmpty()) View.VISIBLE else View.GONE
}
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

private fun String.toCategoryLabel(): String = when (uppercase()) {
"FREE" -> "자유"
"QUESTION" -> "질문"
"INFO" -> "정보"
"BRAG" -> "자랑"
"REVIEW" -> "후기"
else -> this
}
Loading
Loading