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
Expand Up @@ -12,15 +12,17 @@ import jp.wasabeef.transformers.coil.BlurTransformation

object BindingAdapter {
@JvmStatic
@BindingAdapter("loads3ImageUrl")
@BindingAdapter(value = ["loads3ImageUrl", "cornerRadius"], requireAll = false)
fun loadS3ImageUrl(
view: ImageView,
s3ImageKey: String,
cornerRadius: Float?,
) {
val fullUrl: String = view.getS3ImageUrl(s3ImageKey)

view.load(fullUrl) {
error(img_loading_thumbnail)
if (cornerRadius != null) transformations(RoundedCornersTransformation(cornerRadius.toFloatPxFromDp()))
}
}
Comment on lines +15 to 27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt | head -40

Repository: 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:

  • Declare reference params as nullable and guard: fun View.bindFoo(value: CharSequence?) { val v = value ?: default; ... }. (medium.com)
  • For primitives you want “no value” behavior, use boxed nullable types (Int?) so Data Binding will pass null instead of 0. (medium.com)

If you want, tell me a specific BindingAdapter signature or XML case and I’ll show the exact Kotlin fix.

Citations:


🏁 Script executed:

find . -type f -name "*.xml" | xargs grep -l "loads3ImageUrl" 2>/dev/null | head -5

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:

  • Coil ImageView.load / ImageRequest builder docs (shows transformations as a request property). [1]
  • ImageRequest docs / builder immutability note — use newBuilder() to start from an existing request if you want to preserve/extend settings. [2]

🏁 Script executed:

grep -A 5 -B 5 "loads3ImageUrl\|cornerRadius" ./app/src/main/res/layout/dialog_avatar_change.xml

Repository: 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.xml

Repository: Team-WSS/WSS-Android

Length of output: 674


🏁 Script executed:

# Check all layout files for complete usage patterns
find . -type f -name "*.xml" -exec grep -l "cornerRadius\|loads3ImageUrl" {} \; 2>/dev/null | head -10

Repository: Team-WSS/WSS-Android

Length of output: 593


🏁 Script executed:

# Check if any layout uses cornerRadius without loads3ImageUrl
for file in ./app/src/main/res/layout/dialog_avatar_change.xml ./app/src/main/res/layout/dialog_novel_detail_cover.xml ./app/src/main/res/layout/item_blocked_user.xml ./app/src/main/res/layout/item_common_keyword.xml ./app/src/main/res/layout/item_detail_explore_result.xml ./app/src/main/res/layout/item_feed_detail_comment.xml ./app/src/main/res/layout/item_my_activity.xml ./app/src/main/res/layout/item_normal_explore.xml ./app/src/main/res/layout/item_popular_novel.xml ./app/src/main/res/layout/item_recommended_novel_by_user_taste.xml; do
  echo "=== $file ==="
  grep -A 10 -B 2 "cornerRadius\|loads3ImageUrl" "$file" 2>/dev/null | head -20
done

Repository: Team-WSS/WSS-Android

Length of output: 6904


🏁 Script executed:

# Check the complete loadImageWithOptions function to understand if there's a pattern for handling multiple transformations
cat -n app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt | sed -n '29,60p'

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@BindingAdapter(value = ["loads3ImageUrl", "cornerRadius"], requireAll = false)
fun loadS3ImageUrl(
view: ImageView,
s3ImageKey: String,
cornerRadius: Float?,
) {
val fullUrl: String = view.getS3ImageUrl(s3ImageKey)
view.load(fullUrl) {
error(img_loading_thumbnail)
if (cornerRadius != null) transformations(RoundedCornersTransformation(cornerRadius.toFloatPxFromDp()))
}
}
@BindingAdapter(value = ["loads3ImageUrl", "cornerRadius"], requireAll = false)
fun loadS3ImageUrl(
view: ImageView,
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()))
}
}
🤖 Prompt for AI Agents
In @app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt around
lines 15 - 27, The BindingAdapter loadS3ImageUrl is declared with a non-null
s3ImageKey but requireAll=false can pass null and cause an NPE; change the
parameter s3ImageKey to nullable (String?) and guard at the start of
loadS3ImageUrl (e.g., if s3ImageKey.isNullOrEmpty() return or clear the
ImageView/set placeholder) before calling view.getS3ImageUrl and view.load;
mirror the safe pattern used in loadImageWithOptions to avoid calling extension
methods with a null key.


Expand Down
34 changes: 21 additions & 13 deletions app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package com.into.websoso.data.mapper

import com.into.websoso.data.model.CommentEntity
import com.into.websoso.data.model.CommentsEntity
import com.into.websoso.data.model.FeedDetailEntity
import com.into.websoso.data.model.FeedDetailEntity.NovelEntity
import com.into.websoso.data.model.FeedDetailEntity.UserEntity
import com.into.websoso.data.model.FeedEntity
import com.into.websoso.data.model.FeedEntity.NovelEntity
import com.into.websoso.data.model.FeedEntity.UserEntity
import com.into.websoso.data.model.FeedsEntity
import com.into.websoso.data.model.PopularFeedsEntity
import com.into.websoso.data.model.UserInterestFeedsEntity
Expand All @@ -25,7 +26,7 @@ fun FeedsResponseDto.toData(): FeedsEntity =

fun FeedResponseDto.toData(): FeedEntity =
FeedEntity(
user = UserEntity(
user = FeedEntity.UserEntity(
id = userId,
nickname = nickname,
avatarImage = avatarImage,
Expand All @@ -43,7 +44,7 @@ fun FeedResponseDto.toData(): FeedEntity =
isPublic = isPublic,
images = thumbnailUrl?.let { listOf(it) } ?: emptyList(),
imageCount = imageCount,
novel = NovelEntity(
novel = FeedEntity.NovelEntity(
id = novelId,
title = title,
rating = novelRating,
Expand All @@ -59,7 +60,7 @@ fun CommentsResponseDto.toData(): CommentsEntity =

fun CommentResponseDto.toData(): CommentEntity =
CommentEntity(
user = UserEntity(
user = FeedEntity.UserEntity(
id = userId,
nickname = nickname,
avatarImage = avatarImage,
Expand All @@ -74,8 +75,8 @@ fun CommentResponseDto.toData(): CommentEntity =
isSpoiler = isSpoiler,
)

fun FeedDetailResponseDto.toData(): FeedEntity =
FeedEntity(
fun FeedDetailResponseDto.toData(): FeedDetailEntity =
FeedDetailEntity(
user = UserEntity(
id = userId,
nickname = nickname,
Expand All @@ -94,12 +95,19 @@ fun FeedDetailResponseDto.toData(): FeedEntity =
isPublic = isPublic,
images = images,
imageCount = images.size,
novel = NovelEntity(
id = novelId,
title = title,
rating = novelRating,
ratingCount = novelRatingCount,
),
novel = novelId?.let { id ->
NovelEntity(
id = id,
title = title.orEmpty(),
rating = novelRating,
ratingCount = novelRatingCount ?: 0,
thumbnail = novelThumbnailImage.orEmpty(),
genre = novelGenre.orEmpty(),
author = novelAuthor.orEmpty(),
description = novelDescription.orEmpty(),
feedWriterNovelRating = feedWriterNovelRating,
)
},
)

fun PopularFeedsResponseDto.toData(): PopularFeedsEntity =
Expand Down
40 changes: 40 additions & 0 deletions app/src/main/java/com/into/websoso/data/model/FeedDetailEntity.kt
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
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "FeedDetailResponseDto.kt" -type f

Repository: Team-WSS/WSS-Android

Length of output: 261


🏁 Script executed:

git ls-files "*.kt" | grep -i "feeddetail\|FeedDetail" | head -20

Repository: Team-WSS/WSS-Android

Length of output: 1634


🌐 Web query:

kotlinx.serialization nullable field without default value missing from JSON MissingFieldException

💡 Result:

Why it happens (short)

  • By default kotlinx.serialization treats a property as "required" unless it has a default value. A nullable property without a default is considered non-optional, so if the JSON omits it the generated serializer throws MissingFieldException. [1][2]

How to fix (pick one)

  • Give the property a default (e.g. val description: String? = null). That makes the element optional in the descriptor and prevents the exception. [2]
  • Or configure Json to treat missing nullable fields as nulls: Json { explicitNulls = false } — with this setting absence of a nullable field is interpreted as null during decoding. (Be careful: this changes encoding/decoding symmetry for nullable-with-non-null-default cases.) [1]
  • Alternatively, use coerceInputValues for some invalid inputs to be treated as missing (not a general fix for omitted fields). [3]

References

  • explicitNulls behavior and example. [1]
  • MissingFieldException / optional element rules (default values → optional). [2]
  • coerceInputValues docs. [3]

🏁 Script executed:

cat -n app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.kt

Repository: Team-WSS/WSS-Android

Length of output: 2068


🏁 Script executed:

rg "Json\s*\{" -A 10 --type kt

Repository: Team-WSS/WSS-Android

Length of output: 90


🏁 Script executed:

rg "Json\s*\{" -A 10 --type-list | head -5

Repository: Team-WSS/WSS-Android

Length of output: 453


🏁 Script executed:

find . -name "*.kt" -type f -exec grep -l "Json\s*{" {} \; | head -10

Repository: Team-WSS/WSS-Android

Length of output: 142


🏁 Script executed:

rg "explicitNulls|ignoreUnknownKeys" --type kt

Repository: Team-WSS/WSS-Android

Length of output: 90


🏁 Script executed:

cat core/network/src/main/java/com/into/websoso/core/network/di/NetworkModule.kt

Repository: 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 역호환성: 새 필드에 = null 기본값 필요

현재 새 필드들(novelThumbnailImage, novelGenre, novelAuthor, feedWriterNovelRating, novelDescription)이 String?/Float? 타입이지만 기본값이 없어, 서버가 해당 키를 내려주지 않는 경우(구버전 응답 등) 디코딩 시 MissingFieldException이 발생합니다.

ignoreUnknownKeys = true는 JSON의 여분 키만 무시하고, 누락된 필수 필드는 여전히 예외를 발생시킵니다. 역호환성을 위해 각 필드에 = null 기본값을 추가하세요.

제안 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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?,
@SerialName("novelThumbnailImage")
val novelThumbnailImage: String? = null,
@SerialName("novelGenre")
val novelGenre: String? = null,
@SerialName("novelAuthor")
val novelAuthor: String? = null,
@SerialName("feedWriterNovelRating")
val feedWriterNovelRating: Float? = null,
@SerialName("novelDescription")
val novelDescription: String? = null,
🤖 Prompt for AI Agents
In
@app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.kt
around lines 46 - 55, The new nullable properties in FeedDetailResponseDto
(novelThumbnailImage, novelGenre, novelAuthor, feedWriterNovelRating,
novelDescription) lack default values and will cause kotlinx.serialization
MissingFieldException when the server omits them; update each property
declaration in the FeedDetailResponseDto primary constructor to provide a
default of = null so they become truly optional during decoding.

)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.into.websoso.data.library.datasource.LibraryLocalDataSource
import com.into.websoso.data.mapper.MultiPartMapper
import com.into.websoso.data.mapper.toData
import com.into.websoso.data.model.CommentsEntity
import com.into.websoso.data.model.FeedDetailEntity
import com.into.websoso.data.model.FeedEntity
import com.into.websoso.data.model.FeedsEntity
import com.into.websoso.data.model.PopularFeedsEntity
Expand Down Expand Up @@ -125,7 +126,7 @@ class FeedRepository
}
}

suspend fun fetchFeed(feedId: Long): FeedEntity = feedApi.getFeed(feedId).toData()
suspend fun fetchFeed(feedId: Long): FeedDetailEntity = feedApi.getFeed(feedId).toData()

suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

IllegalArgumentException()에 메시지/원인 없이 throw (detekt 경고)
디버깅 가능성을 위해 어떤 값이 왜 필요한지 메시지를 넣는 게 좋습니다.

🛠️ 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
In @app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt
around lines 340 - 355, The code currently throws plain
IllegalArgumentException() when menuType is null in the
DialogRemovePopupMenuBinding and DialogReportPopupMenuBinding branches; update
the throws (or use requireNotNull) to include a descriptive message such as
"menuType is null when creating FeedRemoveDialogFragment" and "menuType is null
when creating FeedReportDialogFragment" (or similar) so the exception identifies
which fragment and parameter failed; adjust the two calls inside
FeedDetailActivity where FeedRemoveDialogFragment.newInstance and
FeedReportDialogFragment.newInstance are invoked.

}

Expand All @@ -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()
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Null 안전성 처리에 일관성이 필요합니다.

Line 70에서 uiFeed를 직접 반환하지만, Line 72와 77에서는 uiFeed?.copy로 null-safe 호출을 사용합니다. Line 68에서 feedDetail.toUi().feed가 nullable을 반환하는 경우, Line 70의 분기에서도 null 처리가 필요합니다.

🛡️ 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,
                             )
                         }

또는 uiFeed가 null이 될 수 없다면, Line 68 이후에 null 체크를 추가하는 것을 고려해주세요.

🤖 Prompt for AI Agents
In @app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailViewModel.kt
around lines 65 - 81, feedDetail.toUi().feed을 uiFeed에 할당한 후 분기에서 null 처리가 일관되지
않아 NPE 가능성이 있으니, uiFeed가 nullable인지 확정하고 일관된 처리를 적용하세요: 만약 uiFeed가 null일 수 있다면
기존의 uiFeed?.copy(...) 패턴처럼 모든 분기에서 안전 호출을 사용하거나 early return/대체값(예: null 또는 기본
Feed 객체)으로 처리하고, uiFeed가 절대 null이 될 수 없다면 val uiFeed = feedDetail.toUi().feed!!로
명시적 non-null 보장을 하여 updatedFeed 분기(현재의 첫 분기에서 직접 uiFeed 반환, 다른 분기에서는
uiFeed?.copy 사용)들을 일관되게 정리하세요.


Expand All @@ -89,6 +89,7 @@ class FeedDetailViewModel
user = FeedDetailModel.UserModel(
avatarImage = myProfile.avatarImage,
),
novel = feedDetail.novel,
),
)
}.onFailure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class FeedDetailAdapter(
) {
when (holder) {
is FeedDetailCommentViewHolder -> holder.bind((getItem(position) as Comment).comment)
is FeedDetailContentViewHolder -> holder.bind((getItem(position) as Header).feed)
is FeedDetailContentViewHolder -> holder.bind((getItem(position) as Header).feedDetail)
}
}

Expand All @@ -58,10 +58,13 @@ class FeedDetailAdapter(
newItem: FeedDetailType,
): Boolean =
when {
(oldItem is Comment) and (newItem is Comment) ->
(oldItem is Comment) and (newItem is Comment) -> {
(oldItem as Comment).comment.commentId == (newItem as Comment).comment.commentId
}

else -> oldItem == newItem
else -> {
oldItem == newItem
}
}

override fun areContentsTheSame(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

잠재적인 NPE 위험이 있습니다.

Line 26에서 feedDetail.feed?.imageUrls가 null이면 early return하지만, Line 27의 onFeedImageClick 람다 내부에서 feedDetail.feed.imageUrls를 null-safe 연산자 없이 접근합니다. feedDetail.feed가 null인 경우 NPE가 발생할 수 있습니다.

🐛 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
}
}
binding.cvFeedImage.setContent {
val imageUrls = feedDetail.feed?.imageUrls ?: return@setContent
AdaptationFeedImageContainer(imageUrls) { index ->
feedDetailClickListener.onFeedImageClick(index, imageUrls)
}
}
🤖 Prompt for AI Agents
In
@app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt
around lines 25 - 29, The code calls binding.cvFeedImage.setContent with
AdaptationFeedImageContainer using feedDetail.feed?.imageUrls for early-return,
but the lambda passed to AdaptationFeedImageContainer references
feedDetail.feed.imageUrls without null-safety which can NPE; fix by capturing
the non-null image list into a local val (e.g., val images =
feedDetail.feed?.imageUrls ?: return@setContent) before calling
AdaptationFeedImageContainer and then use that local images inside the
onFeedImageClick lambda when invoking
feedDetailClickListener.onFeedImageClick(index, images).

binding.ivFeedDetailNovelGenre.setImageResource(feedDetail.novelImage)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.into.websoso.ui.feedDetail.adapter

import com.into.websoso.ui.feedDetail.model.CommentModel
import com.into.websoso.ui.main.feed.model.FeedModel
import com.into.websoso.ui.feedDetail.model.FeedDetailModel

sealed interface FeedDetailType {
data class Header(
val feed: FeedModel,
val feedDetail: FeedDetailModel,
) : FeedDetailType

data class Comment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.into.websoso.ui.feedDetail.component

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand All @@ -13,7 +13,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.into.websoso.core.common.ui.component.AdaptationImage
import com.into.websoso.core.common.util.clickableWithoutRipple
import com.into.websoso.ui.feedDetail.model.ImageContainerType.DOUBLE

@Composable
fun DoubleImageContainer(
Expand All @@ -24,18 +23,17 @@ fun DoubleImageContainer(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
imageUrls.take(DOUBLE.size).forEach { imageUrl ->
imageUrls.take(2).forEachIndexed { index, imageUrl ->
AdaptationImage(
imageUrl = imageUrl,
contentScale = ContentScale.Crop,
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.size(100.dp)
.clip(RoundedCornerShape(8.dp))
.clickableWithoutRipple {
onImageClick(imageUrls.indexOf(imageUrl))
onImageClick(index)
},
)
}
Expand Down
Loading
Loading