-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 피드 상세보기 뷰 수정 #782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 피드 상세보기 뷰 수정 #782
Changes from all commits
7bc331f
0e974d3
6ea774c
97e89a4
5af6fd2
139a389
157c743
0e29d8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.into.websoso.data.model | ||
|
|
||
| data class FeedDetailEntity( | ||
| val user: UserEntity, | ||
| val createdDate: String, | ||
| val id: Long, | ||
| val content: String, | ||
| val relevantCategories: List<String>, | ||
| val likeCount: Int, | ||
| val isLiked: Boolean, | ||
| val commentCount: Int, | ||
| val isModified: Boolean, | ||
| val isSpoiler: Boolean, | ||
| val isMyFeed: Boolean, | ||
| val isPublic: Boolean, | ||
| val novel: NovelEntity?, | ||
| val images: List<String>, | ||
| val imageCount: Int, | ||
| ) { | ||
| data class UserEntity( | ||
| val id: Long, | ||
| val nickname: String, | ||
| val avatarImage: String, | ||
| ) | ||
|
|
||
| data class NovelEntity( | ||
| val id: Long, | ||
| val title: String, | ||
| val rating: Float?, | ||
| val ratingCount: Int, | ||
| val thumbnail: String, | ||
| val genre: String, | ||
| val author: String, | ||
| val description: String, | ||
| val feedWriterNovelRating: Float?, | ||
| ) { | ||
| val isRatingNoting: Boolean = rating == null | ||
| val isWriterRatingNoting: Boolean = feedWriterNovelRating == null | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,4 +43,14 @@ data class FeedDetailResponseDto( | |||||||||||||||||||||||||||||||||||||||||
| val isPublic: Boolean, | ||||||||||||||||||||||||||||||||||||||||||
| @SerialName("images") | ||||||||||||||||||||||||||||||||||||||||||
| val images: List<String>, | ||||||||||||||||||||||||||||||||||||||||||
| @SerialName("novelThumbnailImage") | ||||||||||||||||||||||||||||||||||||||||||
| val novelThumbnailImage: String?, | ||||||||||||||||||||||||||||||||||||||||||
| @SerialName("novelGenre") | ||||||||||||||||||||||||||||||||||||||||||
| val novelGenre: String?, | ||||||||||||||||||||||||||||||||||||||||||
| @SerialName("novelAuthor") | ||||||||||||||||||||||||||||||||||||||||||
| val novelAuthor: String?, | ||||||||||||||||||||||||||||||||||||||||||
| @SerialName("feedWriterNovelRating") | ||||||||||||||||||||||||||||||||||||||||||
| val feedWriterNovelRating: Float?, | ||||||||||||||||||||||||||||||||||||||||||
| @SerialName("novelDescription") | ||||||||||||||||||||||||||||||||||||||||||
| val novelDescription: String?, | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "FeedDetailResponseDto.kt" -type fRepository: Team-WSS/WSS-Android Length of output: 261 🏁 Script executed: git ls-files "*.kt" | grep -i "feeddetail\|FeedDetail" | head -20Repository: Team-WSS/WSS-Android Length of output: 1634 🌐 Web query:
💡 Result: Why it happens (short)
How to fix (pick one)
References
🏁 Script executed: cat -n app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.ktRepository: Team-WSS/WSS-Android Length of output: 2068 🏁 Script executed: rg "Json\s*\{" -A 10 --type ktRepository: Team-WSS/WSS-Android Length of output: 90 🏁 Script executed: rg "Json\s*\{" -A 10 --type-list | head -5Repository: Team-WSS/WSS-Android Length of output: 453 🏁 Script executed: find . -name "*.kt" -type f -exec grep -l "Json\s*{" {} \; | head -10Repository: Team-WSS/WSS-Android Length of output: 142 🏁 Script executed: rg "explicitNulls|ignoreUnknownKeys" --type ktRepository: Team-WSS/WSS-Android Length of output: 90 🏁 Script executed: cat core/network/src/main/java/com/into/websoso/core/network/di/NetworkModule.ktRepository: Team-WSS/WSS-Android Length of output: 2562 🏁 Script executed: rg "explicitNulls|ignoreUnknownKeys|Json"Repository: Team-WSS/WSS-Android Length of output: 50379 kotlinx.serialization 역호환성: 새 필드에 현재 새 필드들(
제안 diff @SerialName("novelThumbnailImage")
- val novelThumbnailImage: String?,
+ val novelThumbnailImage: String? = null,
@SerialName("novelGenre")
- val novelGenre: String?,
+ val novelGenre: String? = null,
@SerialName("novelAuthor")
- val novelAuthor: String?,
+ val novelAuthor: String? = null,
@SerialName("feedWriterNovelRating")
- val feedWriterNovelRating: Float?,
+ val feedWriterNovelRating: Float? = null,
@SerialName("novelDescription")
- val novelDescription: String?,
+ val novelDescription: String? = null,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -337,19 +337,21 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed | |
| noinline event: () -> Unit, | ||
| ) { | ||
| when (Dialog::class) { | ||
| DialogRemovePopupMenuBinding::class -> | ||
| DialogRemovePopupMenuBinding::class -> { | ||
| FeedRemoveDialogFragment | ||
| .newInstance( | ||
| menuType = menuType ?: throw IllegalArgumentException(), | ||
| event = { event() }, | ||
| ).show(supportFragmentManager, FeedRemoveDialogFragment.TAG) | ||
| } | ||
|
|
||
| DialogReportPopupMenuBinding::class -> | ||
| DialogReportPopupMenuBinding::class -> { | ||
| FeedReportDialogFragment | ||
| .newInstance( | ||
| menuType = menuType ?: throw IllegalArgumentException(), | ||
| event = { event() }, | ||
| ).show(supportFragmentManager, FeedReportDialogFragment.TAG) | ||
| } | ||
| } | ||
|
Comment on lines
+340
to
355
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛠️ Proposed fix menuType = menuType ?: throw IllegalArgumentException(),
+// ->
+menuType = menuType ?: throw IllegalArgumentException("menuType is required for ${Dialog::class.simpleName}"),🧰 Tools🪛 detekt (1.23.8)[warning] 343-343: A call to the default constructor of an exception was detected. Instead one of the constructor overloads should be called. This allows to provide more meaningful exceptions. (detekt.exceptions.ThrowingExceptionsWithoutMessageOrCause) [warning] 351-351: A call to the default constructor of an exception was detected. Instead one of the constructor overloads should be called. This allows to provide more meaningful exceptions. (detekt.exceptions.ThrowingExceptionsWithoutMessageOrCause) 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
|
|
@@ -371,7 +373,9 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed | |
| NovelDetailBack.RESULT_OK, | ||
| CreateFeed.RESULT_OK, | ||
| OtherUserProfileBack.RESULT_OK, | ||
| -> feedDetailViewModel.updateFeedDetail(feedId, CreateFeed) | ||
| -> { | ||
| feedDetailViewModel.updateFeedDetail(feedId, CreateFeed) | ||
| } | ||
|
|
||
| BlockUser.RESULT_OK -> { | ||
| val nickname = result.data?.getStringExtra(USER_NICKNAME).orEmpty() | ||
|
|
@@ -482,18 +486,22 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed | |
| private fun setupObserver() { | ||
| feedDetailViewModel.feedDetailUiState.observe(this) { feedDetailUiState -> | ||
| when { | ||
| feedDetailUiState.loading -> binding.wllFeed.setWebsosoLoadingVisibility(true) | ||
| feedDetailUiState.loading -> { | ||
| binding.wllFeed.setWebsosoLoadingVisibility(true) | ||
| } | ||
|
|
||
| feedDetailUiState.error -> { | ||
| binding.wllFeed.setLoadingLayoutVisibility(false) | ||
|
|
||
| when (feedDetailUiState.previousStack.from) { | ||
| CreateFeed, FeedDetailRefreshed -> | ||
| CreateFeed, FeedDetailRefreshed -> { | ||
| RemovedFeedDialogFragment | ||
| .newInstance { | ||
| val extraIntent = Intent().apply { putExtra(FEED_ID, feedId) } | ||
| setResult(FeedDetailRefreshed.RESULT_OK, extraIntent) | ||
| if (!isFinishing) finish() | ||
| }.show(supportFragmentManager, RemovedFeedDialogFragment.TAG) | ||
| } | ||
|
|
||
| else -> { | ||
| val extraIntent = Intent().apply { putExtra(FEED_ID, feedId) } | ||
|
|
@@ -553,7 +561,7 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed | |
| } | ||
| } | ||
|
|
||
| val header = feedDetailUiState.feedDetail.feed?.let { Header(it) } | ||
| val header = Header(feedDetailUiState.feedDetail) | ||
| val comments = feedDetailUiState.feedDetail.comments.map { Comment(it) } | ||
| val feedDetail = listOf(header) + comments | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel | |
| import androidx.lifecycle.viewModelScope | ||
| import com.into.websoso.core.common.ui.model.ResultFrom | ||
| import com.into.websoso.data.model.CommentsEntity | ||
| import com.into.websoso.data.model.FeedEntity | ||
| import com.into.websoso.data.model.FeedDetailEntity | ||
| import com.into.websoso.data.model.MyProfileEntity | ||
| import com.into.websoso.data.repository.FeedRepository | ||
| import com.into.websoso.data.repository.NotificationRepository | ||
|
|
@@ -62,21 +62,21 @@ class FeedDetailViewModel | |
| } | ||
| }.onSuccess { result -> | ||
| val myProfile = result[0] as MyProfileEntity | ||
| val feed = (result[1] as FeedEntity) | ||
| val feedDetail = (result[1] as FeedDetailEntity) | ||
| val comments = result[2] as CommentsEntity | ||
|
|
||
| val uiFeed = feed.toUi() | ||
| val updatedFeed = if (feed.isLiked == isLiked) { | ||
| val uiFeed = feedDetail.toUi().feed | ||
| val updatedFeed = if (feedDetail.isLiked == isLiked) { | ||
| uiFeed | ||
| } else if (!isLiked && feed.isLiked) { | ||
| uiFeed.copy( | ||
| } else if (!isLiked && feedDetail.isLiked) { | ||
| uiFeed?.copy( | ||
| isLiked = false, | ||
| likeCount = feed.likeCount - 1, | ||
| likeCount = feedDetail.likeCount - 1, | ||
| ) | ||
| } else { | ||
| uiFeed.copy( | ||
| uiFeed?.copy( | ||
| isLiked = true, | ||
| likeCount = feed.likeCount + 1, | ||
| likeCount = feedDetail.likeCount + 1, | ||
| ) | ||
| } | ||
|
Comment on lines
+65
to
81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Null 안전성 처리에 일관성이 필요합니다. Line 70에서 🛡️ Null 안전성 개선 제안- val updatedFeed = if (feedDetail.isLiked == isLiked) {
- uiFeed
- } else if (!isLiked && feedDetail.isLiked) {
+ val updatedFeed = if (feedDetail.isLiked == isLiked) {
+ uiFeed
+ } else if (!isLiked && feedDetail.isLiked) {
uiFeed?.copy(
isLiked = false,
likeCount = feedDetail.likeCount - 1,
)
} else {
uiFeed?.copy(
isLiked = true,
likeCount = feedDetail.likeCount + 1,
)
}또는 🤖 Prompt for AI Agents |
||
|
|
||
|
|
@@ -89,6 +89,7 @@ class FeedDetailViewModel | |
| user = FeedDetailModel.UserModel( | ||
| avatarImage = myProfile.avatarImage, | ||
| ), | ||
| novel = feedDetail.novel, | ||
| ), | ||
| ) | ||
| }.onFailure { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,7 +7,7 @@ import com.into.websoso.core.common.util.getS3ImageUrl | |||||||||||||||||||||||||||
| import com.into.websoso.databinding.ItemFeedDetailHeaderBinding | ||||||||||||||||||||||||||||
| import com.into.websoso.ui.feedDetail.FeedDetailClickListener | ||||||||||||||||||||||||||||
| import com.into.websoso.ui.feedDetail.component.AdaptationFeedImageContainer | ||||||||||||||||||||||||||||
| import com.into.websoso.ui.main.feed.model.FeedModel | ||||||||||||||||||||||||||||
| import com.into.websoso.ui.feedDetail.model.FeedDetailModel | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| class FeedDetailContentViewHolder( | ||||||||||||||||||||||||||||
| private val feedDetailClickListener: FeedDetailClickListener, | ||||||||||||||||||||||||||||
|
|
@@ -17,16 +17,17 @@ class FeedDetailContentViewHolder( | |||||||||||||||||||||||||||
| binding.onClick = feedDetailClickListener | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| fun bind(feed: FeedModel) { | ||||||||||||||||||||||||||||
| binding.feed = feed.copy( | ||||||||||||||||||||||||||||
| user = feed.user.copy(avatarImage = itemView.getS3ImageUrl(feed.user.avatarImage)), | ||||||||||||||||||||||||||||
| fun bind(feedDetail: FeedDetailModel) { | ||||||||||||||||||||||||||||
| binding.feedDetail = feedDetail.copy( | ||||||||||||||||||||||||||||
| user = feedDetail.user?.copy(avatarImage = itemView.getS3ImageUrl(feedDetail.user.avatarImage)), | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| binding.clFeedLike.isSelected = feed.isLiked | ||||||||||||||||||||||||||||
| binding.clFeedLike.isSelected = feedDetail.feed?.isLiked == true | ||||||||||||||||||||||||||||
| binding.cvFeedImage.setContent { | ||||||||||||||||||||||||||||
| AdaptationFeedImageContainer(feed.imageUrls) { index -> | ||||||||||||||||||||||||||||
| feedDetailClickListener.onFeedImageClick(index, feed.imageUrls) | ||||||||||||||||||||||||||||
| AdaptationFeedImageContainer(feedDetail.feed?.imageUrls ?: return@setContent) { index -> | ||||||||||||||||||||||||||||
| feedDetailClickListener.onFeedImageClick(index, feedDetail.feed.imageUrls) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
25
to
29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 잠재적인 NPE 위험이 있습니다. Line 26에서 🐛 NPE 방지를 위한 수정 제안 binding.cvFeedImage.setContent {
- AdaptationFeedImageContainer(feedDetail.feed?.imageUrls ?: return@setContent) { index ->
- feedDetailClickListener.onFeedImageClick(index, feedDetail.feed.imageUrls)
+ val imageUrls = feedDetail.feed?.imageUrls ?: return@setContent
+ AdaptationFeedImageContainer(imageUrls) { index ->
+ feedDetailClickListener.onFeedImageClick(index, imageUrls)
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| binding.ivFeedDetailNovelGenre.setImageResource(feedDetail.novelImage) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| companion object { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt | head -40Repository: Team-WSS/WSS-Android
Length of output: 1638
🌐 Web query:
DataBinding requireAll false non-nullable parameter null behavior Android💡 Result:
Short answer: If you use requireAll = false, Data Binding will pass null for omitted reference-type attributes (and JVM default values for primitives). If your @BindingAdapter or setter in Kotlin declares non-nullable parameters, that null will trigger a runtime NPE (Kotlin inserts null-checks for non-null parameters). Make binding-adapter parameters nullable (e.g., Int?, CharSequence?) and handle null to avoid crashes (or box primitives to get null instead of 0). (medium.com)
Recommended patterns:
If you want, tell me a specific BindingAdapter signature or XML case and I’ll show the exact Kotlin fix.
Citations:
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 168
🌐 Web query:
Coil image library transformations() multiple calls overwrite or accumulate💡 Result:
Short answer: successive calls to transformations(...) on an ImageRequest.Builder replace the previous transformations (they do not automatically accumulate). Use one call with multiple Transformation objects (varargs or a list) or build from an existing request (newBuilder()) and set the combined list yourself. [1][2]
Sources:
🏁 Script executed:
grep -A 5 -B 5 "loads3ImageUrl\|cornerRadius" ./app/src/main/res/layout/dialog_avatar_change.xmlRepository: Team-WSS/WSS-Android
Length of output: 625
🏁 Script executed:
grep -A 5 -B 5 "loads3ImageUrl\|cornerRadius" ./app/src/main/res/layout/item_feed_detail_header.xmlRepository: Team-WSS/WSS-Android
Length of output: 674
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 593
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 6904
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 1456
requireAll=false+s3ImageKey: String조합은 DataBinding NPE 크래시 위험cornerRadius만 설정된 레이아웃(예:dialog_avatar_change.xml)에서loads3ImageUrl이 null로 전달되는데, 현재 시그니처가 non-nullable로 선언되어 있어 런타임 NPE가 발생합니다.loadImageWithOptions처럼s3ImageKey: String?로 변경하고 guard 처리하여 안전하게 처리해야 합니다.🛠️ 제안 수정 (nullable + guard)
@BindingAdapter(value = ["loads3ImageUrl", "cornerRadius"], requireAll = false) fun loadS3ImageUrl( view: ImageView, - s3ImageKey: String, + s3ImageKey: String?, cornerRadius: Float?, ) { + if (s3ImageKey.isNullOrBlank()) { + view.setImageResource(img_loading_thumbnail) + return + } val fullUrl: String = view.getS3ImageUrl(s3ImageKey) view.load(fullUrl) { error(img_loading_thumbnail) if (cornerRadius != null) transformations(RoundedCornersTransformation(cornerRadius.toFloatPxFromDp())) } }📝 Committable suggestion
🤖 Prompt for AI Agents