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 @@ -8,9 +8,10 @@ enum class AttractivePoints(
WORLDVIEW("세계관"),
VIBE("분위기"),
MATERIAL("소재"),
WRITINGSKILL("필력"),
;

companion object {
fun fromString(value: String): AttractivePoints? = values().find { it.name.equals(value, ignoreCase = true) }
fun fromString(value: String): AttractivePoints? = entries.find { it.name.equals(value, ignoreCase = true) }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,22 +122,34 @@ class NovelRatingActivity : BaseActivity<ActivityNovelRatingBinding>(activity_no

novelRatingViewModel.uiState.observe(this) { uiState ->
when {
uiState.loading -> binding.wllNovelRating.setWebsosoLoadingVisibility(true)
uiState.loading -> {
binding.wllNovelRating.setWebsosoLoadingVisibility(true)
}

uiState.novelRatingModel.isCharmPointExceed -> handleCharmPointError(uiState)
uiState.novelRatingModel.isCharmPointExceed -> {
handleCharmPointError(uiState)
}

uiState.isFetchError -> binding.wllNovelRating.setErrorLayoutVisibility(true)
uiState.isFetchError -> {
binding.wllNovelRating.setErrorLayoutVisibility(true)
}

uiState.isSaveSuccess -> handleRatingSuccess()
uiState.isSaveSuccess -> {
handleRatingSuccess()
}

uiState.isSaveError -> handleRatingError()
uiState.isSaveError -> {
handleRatingError()
}

isInitialUpdate -> {
isInitialUpdate = false
initView()
}

else -> updateView(uiState)
else -> {
updateView(uiState)
}
}
}
}
Expand Down Expand Up @@ -199,11 +211,16 @@ class NovelRatingActivity : BaseActivity<ActivityNovelRatingBinding>(activity_no
}

private fun updateCharmPointChips(previousSelectedCharmPoints: List<CharmPoint>) {
binding.wcgNovelRatingCharmPoints.forEach { view ->
val chip = view as WebsosoChip
chip.isSelected = previousSelectedCharmPoints.contains(
charmPoints.find { charmPoint -> charmPoint.title == chip.text.toString() },
)
val selectedTitles = previousSelectedCharmPoints.map { it.title }.toSet()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

previousSelectedCharmPoints가 이미 unique한 값들을 가지며 개수가 적다면 toList가 더 빠른 것으로 알고있습니다!

그리고 데이터가 많다면 mapTo(HashSet()) 구문을 써보는 것도 고려하시면 좋을 것 같아요!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

고생하셨습니다

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

이번 케이스는 selectedTitles를 만든 뒤 각 칩에 대해 포함 여부를 여러 번 확인하는 구조라, 생성 비용보다 조회 비용을 줄이는 쪽이 더 적절하다고 판단해 Set은 유지했습니다.

또 mapTo(hashSetOf()) 방식도 생각해봤지만, 현재 데이터 크기가 작고 매력포인트 특성상 크게 확장될 가능성은 낮아 보여 우선은 가독성을 고려해 map { }.toSet() 형태로 유지했습니다.

말씀 주신 덕분에 생성 비용 관점도 함께 생각해볼 수 있었습니다. 감사합니다!


listOf(
binding.wcgNovelRatingCharmPointsRow1,
binding.wcgNovelRatingCharmPointsRow2,
).forEach { chipGroup ->
chipGroup.forEach { view ->
val chip = view as WebsosoChip
chip.isSelected = chip.text.toString() in selectedTitles
}
Comment on lines +220 to +223
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

칩 순회 중 강제 캐스팅은 런타임 크래시 위험이 있습니다.

view as WebsosoChip는 추후 레이아웃 변경으로 다른 View가 섞이면 ClassCastException을 유발할 수 있습니다.

🔧 제안 수정안
         ).forEach { chipGroup ->
             chipGroup.forEach { view ->
-                val chip = view as WebsosoChip
-                chip.isSelected = chip.text.toString() in selectedTitles
+                (view as? WebsosoChip)?.let { chip ->
+                    chip.isSelected = chip.text.toString() in selectedTitles
+                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/into/websoso/ui/novelRating/NovelRatingActivity.kt`
around lines 220 - 223, The current forced cast in the chipGroup.forEach block
(view as WebsosoChip) can throw ClassCastException if a non-WebsosoChip view is
present; change this to a safe check or filtering: either iterate only over
WebsosoChip instances using chipGroup.filterIsInstance<WebsosoChip>() or use a
safe cast like val chip = view as? WebsosoChip ?: return@forEach and then set
chip.isSelected = chip.text.toString() in selectedTitles so non-chip views are
skipped safely (locate the code in NovelRatingActivity.kt inside the
chipGroup.forEach block).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

현재 구조에서는 chipGroup에 createCharmPointChip()으로 생성한 WebsosoChip만 추가된다는 전제로 작성해서 강제 캐스팅을 사용했습니다.

추후 다른 View가 섞일 가능성을 열어두는 방향이라면 방어적으로 처리할 수도 있겠지만, 현재는 잘못된 타입이 들어오는 경우 조용히 넘기기보다 빠르게 드러나는 편이 낫다고 판단했습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@devfeijoa, 납득됩니다. setupCharmPointChips()에서 createCharmPointChip()을 통해서만 칩이 추가되므로 해당 불변식이 코드 구조적으로 보장되어 있고, 잘못된 타입이 유입될 경우 조용히 넘기는 것보다 빠르게 크래시로 드러나게 하는 fail-fast 전략은 타당한 판단입니다. 현재 구조에서는 강제 캐스팅이 적절합니다.

}
}

Expand Down Expand Up @@ -235,22 +252,36 @@ class NovelRatingActivity : BaseActivity<ActivityNovelRatingBinding>(activity_no
}

private fun setupCharmPointChips() {
getString(novel_rating_charm_points).toWrappedCharmPoint().forEach { charmPoint ->
WebsosoChip(this@NovelRatingActivity)
.apply {
setWebsosoChipText(charmPoint.title)
setWebsosoChipTextAppearance(body2)
setWebsosoChipTextColor(bg_novel_rating_chip_text_selector)
setWebsosoChipStrokeColor(bg_novel_rating_chip_stroke_selector)
setWebsosoChipBackgroundColor(bg_novel_rating_chip_background_selector)
setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
setWebsosoChipRadius(20f.toFloatPxFromDp())
setOnWebsosoChipClick { handleCharmPointClick(charmPoint) }
}.also { websosoChip -> binding.wcgNovelRatingCharmPoints.addChip(websosoChip) }
val charmPoints = getString(novel_rating_charm_points).toWrappedCharmPoint()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

val firstRow = charmPoints.take(3)
val secondRow = charmPoints.drop(3)

binding.wcgNovelRatingCharmPointsRow1.removeAllViews()
binding.wcgNovelRatingCharmPointsRow2.removeAllViews()

firstRow.forEach { charmPoint ->
binding.wcgNovelRatingCharmPointsRow1.addChip(createCharmPointChip(charmPoint))
}

secondRow.forEach { charmPoint ->
binding.wcgNovelRatingCharmPointsRow2.addChip(createCharmPointChip(charmPoint))
}
}

private fun createCharmPointChip(charmPoint: CharmPoint): WebsosoChip =
WebsosoChip(this@NovelRatingActivity).apply {
setWebsosoChipText(charmPoint.title)
setWebsosoChipTextAppearance(body2)
setWebsosoChipTextColor(bg_novel_rating_chip_text_selector)
setWebsosoChipStrokeColor(bg_novel_rating_chip_stroke_selector)
setWebsosoChipBackgroundColor(bg_novel_rating_chip_background_selector)
setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
setWebsosoChipRadius(20f.toFloatPxFromDp())
setOnWebsosoChipClick { handleCharmPointClick(charmPoint) }
}

private fun handleCharmPointClick(charmPoint: CharmPoint) {
novelRatingViewModel.updateCharmPoints(charmPoints.find { it == charmPoint } ?: return)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ enum class CharmPoint(
MATERIAL("material", "소재"),
CHARACTER("character", "캐릭터"),
RELATIONSHIP("relationship", "관계"),
WRITINGSKILL("writingskill", "필력"),
;

companion object {
fun String.toWrappedCharmPoint(): List<CharmPoint> {
return split(",").map {
entries.find { charmPoint -> charmPoint.title == it } ?: return emptyList()
fun String.toWrappedCharmPoint(): List<CharmPoint> =
split(",").map { rawTitle ->
val trimmedTitle = rawTitle.trim()
entries.find { charmPoint -> charmPoint.title == trimmedTitle }
?: throw IllegalArgumentException("존재하지 않는 매력포인트입니다: $rawTitle")
}
}

fun String.toFormattedCharmPoint(): String {
entries.forEach { charmPoint ->
Expand Down
34 changes: 26 additions & 8 deletions app/src/main/res/layout/activity_novel_rating.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,35 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_novel_rating_rating" />

<com.into.websoso.core.common.ui.custom.WebsosoChipGroup
android:id="@+id/wcg_novel_rating_charm_points"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/ll_novel_rating_charm_points_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginHorizontal="84dp"
android:layout_marginTop="14dp"
app:chipSpacing="6dp"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_novel_rating_charm_point"
app:singleSelection="false" />
app:layout_constraintTop_toBottomOf="@id/tv_novel_rating_charm_point">

<com.into.websoso.core.common.ui.custom.WebsosoChipGroup
android:id="@+id/wcg_novel_rating_charm_points_row1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacing="6dp"
app:singleLine="true"
app:singleSelection="false" />

<com.into.websoso.core.common.ui.custom.WebsosoChipGroup
android:id="@+id/wcg_novel_rating_charm_points_row2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
app:chipSpacing="6dp"
app:singleLine="true"
app:singleSelection="false" />
</LinearLayout>

<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider_novel_rating_charm_point"
Expand All @@ -174,7 +192,7 @@
app:dividerColor="@color/gray_50_F4F5F8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wcg_novel_rating_charm_points" />
app:layout_constraintTop_toBottomOf="@id/ll_novel_rating_charm_points_container" />

<TextView
android:id="@+id/tv_novel_rating_keyword_title"
Expand Down
13 changes: 13 additions & 0 deletions core/resource/src/main/res/drawable/ic_library_writingskill.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<group>
<clip-path
android:pathData="M5.903,8.281h23v23h-23z"/>
<path
android:pathData="M28.903,8.281C26.577,8.441 10.235,9.997 9.093,21.201C8.954,22.399 8.876,23.602 8.824,24.806L16.859,16.779C17.14,16.498 17.596,16.498 17.877,16.779C18.157,17.059 18.157,17.514 17.877,17.795L6.219,29.441C5.798,29.862 5.798,30.545 6.219,30.965C6.641,31.386 7.324,31.386 7.746,30.965L10.312,28.402C12.203,28.396 14.092,28.288 15.97,28.071C18.373,27.827 20.329,26.882 21.926,25.531H17.391L23.986,23.336C24.491,22.667 24.945,21.956 25.353,21.219H21.708L26.494,18.829C28.377,14.25 28.822,9.461 28.903,8.281Z"
android:fillColor="#C7C7D0"/>
</group>
</vector>
2 changes: 1 addition & 1 deletion core/resource/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
<string name="novel_rating_read_status_watching">보는 중</string>
<string name="novel_rating_read_status_watched">봤어요</string>
<string name="novel_rating_read_status_quit">하차</string>
<string name="novel_rating_charm_points">세계관,분위기,소재,캐릭터,관계</string>
<string name="novel_rating_charm_points">세계관,소재,필력,캐릭터,관계,분위기</string>
<string name="novel_rating_cancel_alert_title">평가를 그만하시겠어요?</string>
<string name="novel_rating_cancel_alert_accept">그만하기</string>
<string name="novel_rating_cancel_alert_cancel">계속 작성</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum class AttractivePoint(
CHARACTER("캐릭터", "character"),
RELATIONSHIP("관계", "relationship"),
VIBE("분위기", "vibe"),
WRITINGSKILL("필력", "writingskill"),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.into.websoso.core.resource.R.drawable.ic_library_quote_started
import com.into.websoso.core.resource.R.drawable.ic_library_relationship
import com.into.websoso.core.resource.R.drawable.ic_library_vibe
import com.into.websoso.core.resource.R.drawable.ic_library_world_view
import com.into.websoso.core.resource.R.drawable.ic_library_writingskill
import com.into.websoso.core.resource.R.drawable.ic_storage_star
import com.into.websoso.domain.library.model.AttractivePoint
import com.into.websoso.domain.library.model.AttractivePoints
Expand Down Expand Up @@ -352,6 +353,7 @@ private fun attractivePointIcon(attractivePoint: AttractivePoint): ImageVector {
AttractivePoint.WORLDVIEW -> ic_library_world_view
AttractivePoint.RELATIONSHIP -> ic_library_relationship
AttractivePoint.VIBE -> ic_library_vibe
AttractivePoint.WRITINGSKILL -> ic_library_writingskill
}
return ImageVector.vectorResource(id = resId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import com.into.websoso.core.resource.R.drawable.ic_library_material
import com.into.websoso.core.resource.R.drawable.ic_library_relationship
import com.into.websoso.core.resource.R.drawable.ic_library_vibe
import com.into.websoso.core.resource.R.drawable.ic_library_world_view
import com.into.websoso.core.resource.R.drawable.ic_library_writingskill
import com.into.websoso.domain.library.model.AttractivePoint
import com.into.websoso.domain.library.model.AttractivePoint.CHARACTER
import com.into.websoso.domain.library.model.AttractivePoint.MATERIAL
import com.into.websoso.domain.library.model.AttractivePoint.RELATIONSHIP
import com.into.websoso.domain.library.model.AttractivePoint.VIBE
import com.into.websoso.domain.library.model.AttractivePoint.WORLDVIEW
import com.into.websoso.domain.library.model.AttractivePoint.WRITINGSKILL
import com.into.websoso.domain.library.model.AttractivePoints

@Composable
Expand Down Expand Up @@ -48,6 +50,14 @@ internal fun LibraryFilterBottomSheetAttractivePoints(
isSelected = attractivePoints[MATERIAL],
onClick = { onAttractivePointClick(MATERIAL) },
)
LibraryFilterBottomSheetClickableItem(
icon = ic_library_writingskill,
iconTitle = "필력",
horizontalPadding = 12.dp,
iconSize = 36.dp,
isSelected = attractivePoints[WRITINGSKILL],
onClick = { onAttractivePointClick(WRITINGSKILL) },
)
LibraryFilterBottomSheetClickableItem(
icon = ic_library_character,
iconTitle = "캐릭터",
Expand Down
Loading