-
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
Conversation
Walkthrough피드 상세 뷰의 소설 정보 표시를 강화하기 위해 전용 FeedDetailEntity 및 FeedDetailModel을 도입하여 엔티티 구조를 재구성합니다. 데이터 계층의 매핑, 저장소, ViewModel, 그리고 UI 컴포넌트들을 업데이트하여 소설 메타데이터를 전파하고, 이미지 렌더링을 개선하며 장르별 드로어블 아이콘을 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant API as Remote API
participant Repo as Repository
participant Mapper as FeedMapper
participant ViewModel as FeedDetailViewModel
participant View as FeedDetailActivity
API->>Repo: FeedDetailResponseDto (novel metadata)
Repo->>Mapper: FeedDetailResponseDto
Mapper->>Mapper: toData() 매핑
Mapper->>ViewModel: FeedDetailEntity (novel 포함)
ViewModel->>ViewModel: toUi() 변환
ViewModel->>ViewModel: novelImage 계산<br/>(Genre.from())
ViewModel->>View: FeedDetailModel 발행
View->>View: binding.feedDetail ← FeedDetailModel
View->>View: 소설 장르 아이콘 표시
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
s9hn
left a comment
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.
어프했습니다
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.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
app/src/main/java/com/into/websoso/ui/feedDetail/component/TripleImageContainer.kt (1)
22-34: 고정 100dp 3개 배치는 화면 폭이 작은 환경에서 잘림 위험
padding(20dp)+100dp * 3+spacedBy(6dp * 2)면 최소 약 352dp가 필요해서, 작은 기기/분할화면/접근성 설정에서 Row가 넘칠 수 있습니다. 디자인이 고정 사이즈를 요구하지 않는다면weight(1f) + aspectRatio(1f)같은 반응형으로 유지하거나,BoxWithConstraints로 가용 폭 기준으로 size를 계산하는 방식이 더 안전합니다.Also applies to: 26-33
app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt (1)
372-379: ActivityResult의 result code별로 올바른 ResultFrom을 넘겨야 함NovelDetailBack, OtherUserProfileBack에서 받은 결과도 모두
CreateFeed로 처리되므로, 에러 발생 시previousStack.from기반 분기(496-497줄)가 왜곡됩니다. CreateFeed와 FeedDetailRefreshed 경로로 모두 빠져 RemovedFeedDialogFragment를 표시하게 되지만, 다른 소스에서 온 경우 다른 동작(FeedDetailRemoved 결과 반환)이 필요할 수 있습니다.result code별로 맞는 ResultFrom을 전달하도록 수정 필요:
제안된 수정
when (result.resultCode) { NovelDetailBack.RESULT_OK, CreateFeed.RESULT_OK, OtherUserProfileBack.RESULT_OK, -> { - feedDetailViewModel.updateFeedDetail(feedId, CreateFeed) + val from = when (result.resultCode) { + NovelDetailBack.RESULT_OK -> NovelDetailBack + CreateFeed.RESULT_OK -> CreateFeed + OtherUserProfileBack.RESULT_OK -> OtherUserProfileBack + else -> Feed + } + feedDetailViewModel.updateFeedDetail(feedId, from) }app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt (1)
30-54: Coiltransformations(...)여러 번 호출 시 마지막 변환만 적용됨Coil의 API에서
transformations()는 누적되지 않으며, 각 호출이 이전의 변환 목록을 교체합니다. 현재 구현에서 cornerRadius, blur, circular 변환이 동시에 활성화되면, 마지막으로 실행된 변환만 남고 이전 변환들은 모두 무시됩니다. 변환들을 리스트에 수집한 후 한 번에 설정해야 합니다.🛠️ 수정 제안
view.load(imageUrl) { crossfade(true) if (isVectorImage == true) decoderFactory(SvgDecoder.Factory()) - if (cornerRadius != null) transformations(RoundedCornersTransformation(cornerRadius.toFloatPxFromDp())) - if (blurRadius != null) transformations(BlurTransformation(view.context, blurRadius)) - if (isCircularImage == true) transformations(CircleCropTransformation()) + val ts = mutableListOf<coil.transform.Transformation>() + if (cornerRadius != null) ts += RoundedCornersTransformation(cornerRadius.toFloatPxFromDp()) + if (blurRadius != null) ts += BlurTransformation(view.context, blurRadius) + if (isCircularImage == true) ts += CircleCropTransformation() + if (ts.isNotEmpty()) transformations(*ts.toTypedArray()) error(img_loading_thumbnail) }app/src/main/res/layout/item_feed.xml (2)
172-183:iv_feed_novel_thumbnail가 실제 “썸네일”이면 이미지 바인딩이 빠진 상태입니다.Line 173~182에서 현재
android:src="@drawable/ic_link"로 고정되어 있어, ID/네이밍(썸네일)과 실제 동작(아이콘)이 어긋날 수 있어요. 썸네일을 보여주는 의도라면loadImageUrl(또는 프로젝트 표준 어댑터)로feed.novel.thumbnail등을 바인딩하는 쪽이 안전합니다. 반대로 아이콘이 의도라면 ID를 아이콘 의미로 유지하는 게 혼동이 덜합니다.✅ (썸네일 의도일 때) 예시 수정안
<ImageView android:id="@+id/iv_feed_novel_thumbnail" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="86dp" + android:layout_height="124dp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" - android:src="@drawable/ic_link" + android:scaleType="centerCrop" + app:loadImageUrl="@{feed.novel.thumbnail}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />Also applies to: 184-200
202-235:@string/feed_score_count(...)포맷 인자 수를 레이아웃들 간에 통일해야 합니다(런타임 크래시 위험).여기서는 Line 217에서
feed_score_count(rating, ratingCount)처럼 2개 인자를 넘기고 있는데, 다른 레이아웃에서는 같은 리소스를 1개 인자로 호출하고 있습니다(아래 파일 참고). 문자열 리소스 placeholder 개수와 호출 인자 개수가 불일치하면MissingFormatArgumentException등으로 터질 수 있어, 리소스를 분리(예: 점수만/점수+카운트)하거나 호출을 통일해 주세요.
🤖 Fix all issues with AI agents
In @app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt:
- Around line 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.
In
@app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.kt:
- Around line 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.
In
@app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt:
- Around line 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).
In
@app/src/main/java/com/into/websoso/ui/feedDetail/component/SingleImageContainer.kt:
- Around line 3-5: The build can fail because Modifier.matchParentSize() used in
SingleImageContainer.kt is missing its import; add the import
androidx.compose.foundation.layout.matchParentSize to the file so the unresolved
reference for Modifier.matchParentSize (used in the SingleImageContainer
composable) is resolved.
In @app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt:
- Around line 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.
In @app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailViewModel.kt:
- Around line 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
사용)들을 일관되게 정리하세요.
In @app/src/main/java/com/into/websoso/ui/feedDetail/model/Genre.kt:
- Around line 6-26: The Genre.from(tag) lookup currently does a case-sensitive
exact match and can fail for inputs like a lowercase "bl"; update the lookup in
the Genre companion object (function from(tag)) to normalize the input and
comparison by trimming and comparing case-insensitively (e.g., normalize both
the incoming tag and Genre.tag via trim() and toLowerCase() or use
equalsIgnoreCase), so entries.find matches regardless of casing/whitespace and
fallback to ROMANCE remains unchanged.
In @app/src/main/res/layout/item_feed_detail_header.xml:
- Around line 12-15: The string resource feed_score_count is being called with
different argument counts across layouts (e.g., item_feed.xml uses two args
while item_feed_detail_header.xml uses one), which can crash at runtime; fix by
splitting the resource into two clearly named entries (e.g., feed_score_only and
feed_score_with_count) and update the data-binding expressions in both layouts
(replace usages of feed_score_count in item_feed.xml and
item_feed_detail_header.xml) to call the appropriate resource with the matching
number of arguments, or alternatively make all calls supply the same argument
list/signature if you prefer a single resource.
In @core/resource/src/main/res/values/strings.xml:
- Line 98: The string resource feed_score_count now expects a single float
argument ("%.1f"), but one binding still passes two args; update the
data-binding expression in the item_feed layout that currently calls
@string/feed_score_count(feed.novel.rating, feed.novel.ratingCount) to pass only
the rating (e.g., @string/feed_score_count(feed.novel.rating)) so it matches the
feed_score_count format and avoids runtime FormatException; verify other usages
already passing one arg remain unchanged.
🧹 Nitpick comments (13)
app/src/main/res/drawable/ic_ln.xml (1)
12-18: 아이콘 경로가 올바르게 정의되었습니다.채팅 버블 모양의 아이콘이 적절한 스트로크 속성으로 구현되어 있습니다. 라운드 처리된 선 연결과 캡 스타일도 잘 적용되었습니다.
선택적으로,
strokeWidth값(1.95653)을 더 간단한 값(예: 1.96 또는 2.0)으로 반올림하여 가독성을 높일 수 있습니다. 다만 디자인 도구에서 내보낸 정확한 값일 수 있으므로 필수 변경은 아닙니다.♻️ (선택사항) strokeWidth 값 단순화
- android:strokeWidth="1.95653" + android:strokeWidth="1.96"또는
- android:strokeWidth="1.95653" + android:strokeWidth="2.0"app/src/main/res/drawable/ic_mystery.xml (2)
2-3: 리소스 추출을 통한 유지보수성 향상을 권장합니다.하드코딩된 색상(
#9560BF,#1A103F)과 크기(70dp)를 리소스 파일로 추출하면 다음과 같은 이점이 있습니다:
- 모든 장르 아이콘에서 일관된 크기와 색상 체계 유지
- 디자인 변경 시 한 곳에서만 수정 가능
- 테마 지원 시 색상 변경 용이
♻️ 리소스 추출 제안
res/values/colors.xml에 색상 추가:<color name="genre_icon_primary">#9560BF</color> <color name="genre_icon_stroke">#1A103F</color>
res/values/dimens.xml에 크기 추가:<dimen name="genre_icon_size">70dp</dimen>그런 다음 이 파일에 적용:
<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="70dp" - android:height="70dp" + android:width="@dimen/genre_icon_size" + android:height="@dimen/genre_icon_size" android:viewportWidth="70" android:viewportHeight="70"> <group> <clip-path android:pathData="M0,0h70v70h-70z"/> <path android:pathData="M69.998,70.001V0.001L-0.002,70.001H69.998Z" - android:fillColor="#9560BF"/> + android:fillColor="@color/genre_icon_primary"/> <path android:pathData="M50.301,50.314H50.312M54.007,54.02C49.095,58.932 43.454,61.255 41.408,59.208C39.361,57.161 41.684,51.521 46.596,46.609C51.508,41.697 57.149,39.374 59.195,41.421C61.242,43.467 58.919,49.108 54.007,54.02ZM54.007,46.608C58.919,51.52 61.242,57.161 59.195,59.208C57.149,61.255 51.508,58.932 46.596,54.02C41.684,49.108 39.361,43.467 41.408,41.42C43.454,39.374 49.095,41.697 54.007,46.608ZM50.826,50.314C50.826,50.604 50.591,50.838 50.301,50.838C50.012,50.838 49.777,50.604 49.777,50.314C49.777,50.025 50.012,49.79 50.301,49.79C50.591,49.79 50.826,50.025 50.826,50.314Z" android:strokeLineJoin="round" android:strokeWidth="1.91217" android:fillColor="#00000000" - android:strokeColor="#1A103F" + android:strokeColor="@color/genre_icon_stroke" android:strokeLineCap="round"/> </group> </vector>Also applies to: 10-11, 17-17
15-15: strokeWidth 값 단순화 고려 (선택사항)
strokeWidth="1.91217"의 정밀도가 과도해 보입니다.1.9또는2.0으로 단순화해도 시각적 차이가 거의 없으며 가독성이 향상됩니다.app/src/main/res/drawable/ic_romance_fantasy.xml (1)
1-16: 벡터 리소스 추가 자체는 문제 없어 보입니다.
viewport/dp크기 일치하고,clip-path도 현재 형태에선 안전합니다. 다만clip-path가 뷰포트 전체를 그대로 자르는 용도라면(항상 동일한 사각형) 리소스 단순화를 위해 제거 가능 여부만 한 번 검토해도 좋겠습니다.app/src/main/res/drawable/ic_feed_detail_writer_star.xml (1)
1-9: 피드 상세 UI용 아이콘으로 깔끔하게 추가되었습니다.
색상#FF675D가 디자인 시스템/테마 색상과 연동돼야 하는 요구가 있다면, 추후color리소스로 분리하는 정도만 고려하면 좋겠습니다.app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt (1)
129-129: 함수명 명확화를 권고합니다.
fetchFeed()의 반환 타입이FeedDetailEntity로 변경되었으므로, 함수명을fetchFeedDetail()로 변경하는 것을 권고합니다. 현재 호출부들(FeedDetailViewModel,CreateFeedViewModel)은FeedDetailEntity의images속성에 접근하므로 타입 호환성 측면에서는 문제가 없으나, 함수명이 의도를 명확히 나타내면 향후 유지보수에 도움이 됩니다.app/src/main/res/drawable/ic_feed_detail_novel_star.xml (1)
1-9: 하드코딩된 색상(#949399)은 리소스 색상으로 치환 고려현재
android:fillColor가 헥스 하드코딩이라 테마/다크모드/디자인 시스템 변경 시 일괄 대응이 어렵습니다. 기존 아이콘들과 동일한 방식이라면 유지해도 되지만, 가능하면@color/...로 통일을 권장합니다.app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt (1)
78-111: novel 필드orEmpty()처리로 “값 없음” 의미가 소실될 수 있음
title/thumbnail/genre/author/description를orEmpty()로 강제하면, API에서 값이 없는 경우와 “빈 문자열”이 의미 있는 경우를 구분할 수 없습니다. UI에서 “없음” 처리가 필요하면String?을 유지하거나, 최소한null/blank 정책을 한 곳(예: UI 매퍼)에서만 결정하는 편이 안전합니다.app/src/main/java/com/into/websoso/data/model/FeedDetailEntity.kt (2)
26-39: 플래그 네이밍(isRatingNoting) 오타/의미 불명확 가능성
isRatingNoting,isWriterRatingNoting은 의도가 “없음(nothing)”인지 “기록 중(noting)”인지 모호합니다. 읽는 쪽 혼동을 줄이도록isRatingMissing/hasRating같은 명확한 네이밍으로 변경을 권장합니다.
3-19:imageCount는 파생값이면 중복 저장 대신 계산 프로퍼티 고려현재는 생성 시 주입받는 구조라
images와 불일치 가능성이 생깁니다. 파생값이면val imageCount get() = images.size형태도 고려해볼 만합니다.app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailAdapter.kt (1)
61-68: 코드 스타일 개선이 적용되었습니다.명시적인 블록 브레이스 추가로 가독성이 향상되었습니다. 비교 로직은 동일하게 유지됩니다.
app/src/main/java/com/into/websoso/ui/feedDetail/model/FeedDetailModel.kt (1)
11-17: UI 모델이 Data 레이어 타입(FeedDetailEntity.NovelEntity)에 직접 의존
장기적으로 모듈/레이어 분리(테스트, 재사용) 관점에서 결합도가 커질 수 있어NovelModel(또는genreTag: String?만)로 끊는 방향을 추천합니다. 또한novel == null을ROMANCE로 폴백하면 의미가 섞일 수 있어 placeholder drawable(예: UNKNOWN)을 고려해도 좋겠습니다.app/src/main/res/layout/item_feed_detail_header.xml (1)
106-113: (권장) ComposeView 폭/제약과 “전체” 하드코딩 문자열을 정리하면 UI 안정성이 좋아집니다.
- Line 108~113:
ComposeView가wrap_content+ start만 걸려 있어, 콘텐츠에 따라 폭/정렬이 흔들릴 수 있습니다(가로 꽉이 목표면0dp+ start/end 제약 고려).- Line 231: "전체"는 문자열 리소스로 빼는 게 안전합니다(i18n).
Also applies to: 227-236
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (32)
app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.ktapp/src/main/java/com/into/websoso/data/mapper/FeedMapper.ktapp/src/main/java/com/into/websoso/data/model/FeedDetailEntity.ktapp/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.ktapp/src/main/java/com/into/websoso/data/repository/FeedRepository.ktapp/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.ktapp/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailViewModel.ktapp/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailAdapter.ktapp/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.ktapp/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailType.ktapp/src/main/java/com/into/websoso/ui/feedDetail/component/DoubleImageContainer.ktapp/src/main/java/com/into/websoso/ui/feedDetail/component/MultipleImageContainer.ktapp/src/main/java/com/into/websoso/ui/feedDetail/component/SingleImageContainer.ktapp/src/main/java/com/into/websoso/ui/feedDetail/component/TripleImageContainer.ktapp/src/main/java/com/into/websoso/ui/feedDetail/model/FeedDetailModel.ktapp/src/main/java/com/into/websoso/ui/feedDetail/model/Genre.ktapp/src/main/java/com/into/websoso/ui/mapper/FeedMapper.ktapp/src/main/res/drawable/btn_feed_novel_info.xmlapp/src/main/res/drawable/ic_bl.xmlapp/src/main/res/drawable/ic_drama.xmlapp/src/main/res/drawable/ic_fantasy.xmlapp/src/main/res/drawable/ic_feed_detail_novel_star.xmlapp/src/main/res/drawable/ic_feed_detail_writer_star.xmlapp/src/main/res/drawable/ic_hf.xmlapp/src/main/res/drawable/ic_ln.xmlapp/src/main/res/drawable/ic_mystery.xmlapp/src/main/res/drawable/ic_romance.xmlapp/src/main/res/drawable/ic_romance_fantasy.xmlapp/src/main/res/drawable/ic_wuxia.xmlapp/src/main/res/layout/item_feed.xmlapp/src/main/res/layout/item_feed_detail_header.xmlcore/resource/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
- app/src/main/java/com/into/websoso/ui/feedDetail/component/MultipleImageContainer.kt
🧰 Additional context used
🧬 Code graph analysis (3)
app/src/main/java/com/into/websoso/ui/feedDetail/component/SingleImageContainer.kt (1)
app/src/main/java/com/into/websoso/core/common/ui/component/AdaptationImage.kt (1)
AdaptationImage(11-32)
app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt (1)
app/src/main/java/com/into/websoso/ui/feedDetail/component/AdaptationFeedImageContainer.kt (1)
AdaptationFeedImageContainer(11-23)
app/src/main/java/com/into/websoso/ui/feedDetail/component/DoubleImageContainer.kt (1)
app/src/main/java/com/into/websoso/core/common/ui/component/AdaptationImage.kt (1)
AdaptationImage(11-32)
🪛 detekt (1.23.8)
app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt
[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)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (23)
app/src/main/res/drawable/ic_ln.xml (2)
1-5: 벡터 드로어블 선언이 올바르게 구성되었습니다.크기(70dp)와 뷰포트(70) 값이 일치하여 좋습니다. 표준 Android 벡터 드로어블 구조를 따르고 있습니다.
6-11: 배경 도형과 클리핑 경로가 적절합니다.대각선 삼각형 배경과 클립 경로가 70x70 캔버스에 맞게 정의되어 있습니다. 장르별 아이콘의 시각적 차별화를 위한 배경 처리가 잘 구현되었습니다.
app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt (1)
9-10:FeedDetailEntityimport 추가: OK
리포지토리에서 상세 엔티티를 반환하도록 바뀐 흐름에 맞는 변경으로 보입니다.app/src/main/res/drawable/ic_wuxia.xml (1)
1-16: 새 장르 아이콘 리소스 추가: OK
70dp/70dp고정 크기라서 실제 사용처ImageView의layout_width/height및scaleType에 따라 의도치 않은 스케일/크롭이 생길 수 있으니, 적용된 화면에서만 한 번 최종 확인하면 충분해 보입니다.app/src/main/res/drawable/ic_fantasy.xml (1)
1-16: 새 장르 아이콘 리소스 추가: OK
다른 장르 아이콘들과 동일한 패턴(clip-path + 2 path)이라 일관성도 좋아 보입니다. 위와 동일하게 실제 렌더링(스케일/크롭)만 화면에서 확인되면 될 것 같습니다.app/src/main/res/drawable/ic_hf.xml (1)
1-16: 벡터 드로어블 리소스가 올바르게 정의되었습니다.장르 아이콘으로 사용될 새로운 벡터 드로어블 리소스가 적절하게 추가되었습니다. XML 구조와 경로 데이터가 Android 표준을 따르고 있습니다.
app/src/main/res/drawable/ic_romance.xml (1)
1-23: 벡터 드로어블 리소스가 올바르게 정의되었습니다.로맨스 장르 아이콘이 적절하게 추가되었습니다. 중첩된 그룹과 복잡한 경로 데이터가 올바르게 구성되어 있습니다.
app/src/main/res/drawable/ic_drama.xml (1)
1-20: 벡터 드로어블 리소스가 올바르게 정의되었습니다.드라마 장르 아이콘이 적절하게 추가되었습니다. fill과 stroke를 함께 사용한 디자인이 올바르게 구현되어 있습니다.
app/src/main/res/drawable/ic_bl.xml (1)
1-23: 벡터 드로어블 리소스가 올바르게 정의되었습니다.BL 장르 아이콘이 적절하게 추가되었습니다. 중첩된 그룹 구조와 경로 데이터가 올바르게 구성되어 있습니다.
app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt (1)
5-7: 임포트/타입 분리 방향은 적절합니다Feed detail 전용 엔티티를 분리해 매핑 경로를 명확히 만든 점은 좋습니다.
app/src/main/res/drawable/btn_feed_novel_info.xml (1)
5-7: 외곽선 스타일 변경은 의도에 맞게 잘 반영됨단색 채움 제거 + stroke 적용으로 더 가벼운 톤이 되었고, 테두리 색상도 리소스로 관리되어 좋습니다. 다만 눌림/비활성 상태가 필요하면 selector로 별도 정의되어 있는지 확인만 해주세요.
app/src/main/java/com/into/websoso/ui/feedDetail/component/TripleImageContainer.kt (1)
28-38: 클릭 인덱스 처리 개선은 좋음(중복 URL/성능 이슈 방지)
forEachIndexed로 index를 전달하는 변경은 안전하고 효율적입니다.app/src/main/java/com/into/websoso/ui/mapper/FeedMapper.kt (1)
86-119:FeedDetailModel의 소설 정보 경로 분산 - 리팩토링 개선 권장
FeedDetailModel.feed.novel(NovelModel: 기본 정보)과FeedDetailModel.novel(NovelEntity: 장르 포함)이 두 경로로 나뉘어 있습니다. 현재는 매퍼에서 동일 소스로 동시에 설정되어 실제 불일치는 없으나, 향후 유지보수 시 한쪽만 수정될 위험이 있습니다.예를 들어
FeedDetailActivity는feed.novel.id/title을 사용하고,novelImage계산 속성은novel?.genre에 의존합니다. 이 구조를 정리하면 좋습니다:
- 옵션 A:
FeedModel.novel을 장르 정보까지 포함하도록 확장- 옵션 B:
FeedDetailModel.novel하나의 경로만 사용하고FeedModel.novel제거[optional_refactor]
app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailType.kt (1)
4-9: LGTM! 타입 마이그레이션이 올바르게 적용되었습니다.
FeedModel에서FeedDetailModel로의 전환이 깔끔하게 처리되었습니다. 프로퍼티명도feedDetail로 변경되어 의미가 더 명확해졌습니다.app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailAdapter.kt (1)
50-50: LGTM! 바인딩 로직이 새로운 모델 구조에 맞게 업데이트되었습니다.
Header.feedDetail을 사용하도록 변경되어FeedDetailType의 프로퍼티 변경사항과 일관성이 유지됩니다.app/src/main/java/com/into/websoso/ui/feedDetail/component/DoubleImageContainer.kt (2)
26-26: 간격 조정이 적용되었습니다.이미지 간 수평 간격이 8dp에서 6dp로 줄어들어 더 밀집된 레이아웃을 제공합니다.
28-39: 이미지 렌더링 및 클릭 처리가 개선되었습니다.여러 개선사항이 적용되었습니다:
take(2)로 안전하게 최대 2개 이미지만 처리forEachIndexed로 인덱스를 직접 전달하여indexOf호출 제거 (성능 향상)size(100.dp)로 고정 크기 적용하여 일관된 UI 제공다양한 화면 밀도에서 100dp 크기가 적절한지 확인해주세요. 특히 작은 화면에서 레이아웃이 잘리지 않는지 테스트가 필요합니다.
app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailViewModel.kt (2)
9-9: 데이터 모델 마이그레이션이 적용되었습니다.
FeedEntity에서FeedDetailEntity로 전환하여 피드 상세 정보를 더 풍부하게 제공할 수 있게 되었습니다.
92-92: 소설 정보가 모델에 추가되었습니다.
feedDetail.novel을FeedDetailModel에 전달하여 소설 메타데이터를 UI에 표시할 수 있게 되었습니다.app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt (2)
20-24: 모델 바인딩이 안전하게 업데이트되었습니다.
FeedDetailModel을 사용하도록 변경되었고, null-safe 접근이 적절히 적용되었습니다:
- 사용자 아바타 URL 변환 시
feedDetail.user?사용- 좋아요 상태 확인 시
feedDetail.feed?.isLiked == true로 null-safe 처리
30-30: 소설 장르 이미지 표시 기능이 추가되었습니다.
feedDetail.novelImage를 사용하여 소설 장르별 아이콘을 표시하는 기능이 추가되었습니다.app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt (1)
489-492: UI 상태 참조가feedDetail기준으로 정리된 점은 좋습니다
헤더 생성이feedDetailUiState.feedDetail로 일관되게 맞춰져 NPE/옵셔널 분기 복잡도가 줄어 보입니다.Also applies to: 497-504, 564-566
app/src/main/res/layout/item_feed_detail_header.xml (1)
140-147:iv_feed_detail_novel_genre은 어댑터 코드에서 이미 설정되고 있습니다.
FeedDetailContentViewHolder.bind()메서드에서binding.ivFeedDetailNovelGenre.setImageResource(feedDetail.novelImage)로 명시적으로 이미지가 설정되고 있습니다.FeedDetailModel의novelImage속성은@DrawableRes Int를 반환하며, 장르 정보가 없을 경우Genre.ROMANCE.drawableRes로 기본값을 제공하므로 런타임에 빈 상태가 될 가능성이 없습니다.
| @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())) | ||
| } | ||
| } |
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:
- 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:
- 1: https://medium.com/android-news/binding-adapters-with-kotlin-part-1-78b957ad6b8b?utm_source=openai
- 2: https://medium.com/android-news/binding-adapters-with-kotlin-part-1-78b957ad6b8b?utm_source=openai
- 3: https://medium.com/android-news/binding-adapters-with-kotlin-part-1-78b957ad6b8b?utm_source=openai
🏁 Script executed:
find . -type f -name "*.xml" | xargs grep -l "loads3ImageUrl" 2>/dev/null | head -5Repository: 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.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:
# Check all layout files for complete usage patterns
find . -type f -name "*.xml" -exec grep -l "cornerRadius\|loads3ImageUrl" {} \; 2>/dev/null | head -10Repository: 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
doneRepository: 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.
| @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.
| @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?, |
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:
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:
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.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 역호환성: 새 필드에 = 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.
| @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.
| 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) | ||
| } | ||
| } |
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.
잠재적인 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.
| 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).
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.size |
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.
matchParentSize() import 누락으로 컴파일 실패 가능
Line 29의 Modifier.matchParentSize()는 import androidx.compose.foundation.layout.matchParentSize가 없으면 unresolved reference가 납니다.
🛠️ Proposed fix
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.matchParentSizeAlso applies to: 20-35
🤖 Prompt for AI Agents
In
@app/src/main/java/com/into/websoso/ui/feedDetail/component/SingleImageContainer.kt
around lines 3 - 5, The build can fail because Modifier.matchParentSize() used
in SingleImageContainer.kt is missing its import; add the import
androidx.compose.foundation.layout.matchParentSize to the file so the unresolved
reference for Modifier.matchParentSize (used in the SingleImageContainer
composable) is resolved.
| 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) | ||
| } | ||
| } |
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.
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.
| 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, | ||
| ) | ||
| } |
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.
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 사용)들을 일관되게 정리하세요.
| enum class Genre( | ||
| val tag: String, | ||
| @DrawableRes val drawableRes: Int, | ||
| ) { | ||
| ROMANCE("romance", R.drawable.ic_romance), | ||
| ROMANCE_FANTASY("romanceFantasy", R.drawable.ic_romance_fantasy), | ||
| BL("BL", R.drawable.ic_bl), | ||
| FANTASY("fantasy", R.drawable.ic_fantasy), | ||
| MODERN_FANTASY(tag = "modernFantasy", drawableRes = R.drawable.ic_hf), | ||
| WUXIA("wuxia", R.drawable.ic_wuxia), | ||
| LIGHT_NOVEL( | ||
| "lightNovel", | ||
| R.drawable.ic_ln, | ||
| ), | ||
| DRAMA("drama", R.drawable.ic_drama), | ||
| MYSTERY("mystery", R.drawable.ic_mystery), | ||
| ; | ||
|
|
||
| companion object { | ||
| fun from(tag: String): Genre = entries.find { it.tag == tag } ?: ROMANCE | ||
| } |
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:
# 프로젝트 Kotlin 버전 확인
find . -name "build.gradle" -o -name "build.gradle.kts" | head -5Repository: Team-WSS/WSS-Android
Length of output: 200
🏁 Script executed:
# build.gradle 또는 build.gradle.kts 파일 검색 및 Kotlin 버전 확인
fd -e "build.gradle*" . | head -10 | xargs -I {} sh -c 'echo "=== {} ===" && grep -i "kotlin" "{}" | head -10'Repository: Team-WSS/WSS-Android
Length of output: 46
🏁 Script executed:
# gradle.properties 확인
fd -name "gradle.properties" . | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'Repository: Team-WSS/WSS-Android
Length of output: 294
🏁 Script executed:
# Genre.from() 호출 위치 확인
rg "Genre\.from\(" --type kotlin -B 2 -A 2Repository: Team-WSS/WSS-Android
Length of output: 580
🏁 Script executed:
# API 통신 관련 코드 검색 - genre 매핑 패턴 확인
rg "genre|Genre" --type kotlin -B 2 -A 2 | grep -A 5 -B 5 "from\|server\|api\|response" | head -50Repository: Team-WSS/WSS-Android
Length of output: 5350
🏁 Script executed:
# app/build.gradle.kts에서 Kotlin 버전 확인
cat app/build.gradle.kts | grep -i "kotlin" | head -20Repository: Team-WSS/WSS-Android
Length of output: 164
🏁 Script executed:
# 최상위 build.gradle.kts 확인
cat build.gradle.kts | grep -i "kotlin" | head -20Repository: Team-WSS/WSS-Android
Length of output: 209
🏁 Script executed:
# 검토 대상 Genre enum 파일 전체 확인
cat app/src/main/java/com/into/websoso/ui/feedDetail/model/Genre.ktRepository: Team-WSS/WSS-Android
Length of output: 848
🏁 Script executed:
# 서버 API 응답 관련 데이터 클래스 확인 (novel/genre 관련)
rg "class.*Novel|data class.*Novel" --type kotlin -A 10 | head -50Repository: Team-WSS/WSS-Android
Length of output: 4930
🏁 Script executed:
# API 응답에서 genre 필드가 어떻게 정의되어 있는지 확인
rg "genre.*:" --type kotlin -B 2 -A 2 | grep -A 5 -B 5 "String\|val genre" | head -40Repository: Team-WSS/WSS-Android
Length of output: 3429
🏁 Script executed:
# FeedDetailModel.kt 파일 확인 - novel 필드 정의
cat app/src/main/java/com/into/websoso/ui/feedDetail/model/FeedDetailModel.ktRepository: Team-WSS/WSS-Android
Length of output: 692
🏁 Script executed:
# libs.versions.toml 또는 gradle.properties에서 Kotlin 버전 확인
find . -name "libs.versions.toml" -o -name "gradle.properties" | xargs cat 2>/dev/null | grep -i "kotlin"Repository: Team-WSS/WSS-Android
Length of output: 1601
🏁 Script executed:
# API 응답 관련 데이터 모델 - genre 필드 타입 확인
rg "val genre|genre:" --type kotlin | grep -v "Genre\." | head -20Repository: Team-WSS/WSS-Android
Length of output: 2428
대소문자 일치 규칙 확인 후 필요시 정규화 추가
from(tag)가 완전일치만 보므로, "BL"처럼 케이스가 섞인 enum 값과 서버에서 소문자로 내려오는 응답이 일치하지 않을 수 있습니다. API 스펙에서 genre 태그 케이싱 규칙을 확인하고, 필요하면 ignoreCase = true 또는 .trim()을 추가하여 안정성을 높이세요.
🛠️ 권장 변경사항
companion object {
- fun from(tag: String): Genre = entries.find { it.tag == tag } ?: ROMANCE
+ fun from(tag: String): Genre =
+ entries.firstOrNull { it.tag.equals(tag.trim(), ignoreCase = true) } ?: ROMANCE
}🤖 Prompt for AI Agents
In @app/src/main/java/com/into/websoso/ui/feedDetail/model/Genre.kt around lines
6 - 26, The Genre.from(tag) lookup currently does a case-sensitive exact match
and can fail for inputs like a lowercase "bl"; update the lookup in the Genre
companion object (function from(tag)) to normalize the input and comparison by
trimming and comparing case-insensitively (e.g., normalize both the incoming tag
and Genre.tag via trim() and toLowerCase() or use equalsIgnoreCase), so
entries.find matches regardless of casing/whitespace and fallback to ROMANCE
remains unchanged.
| <variable | ||
| name="feed" | ||
| type="com.into.websoso.ui.main.feed.model.FeedModel" /> | ||
| name="feedDetail" | ||
| type="com.into.websoso.ui.feedDetail.model.FeedDetailModel" /> | ||
| </data> |
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.
@string/feed_score_count(...) 호출 인자 수 불일치(크래시 가능) — 리소스 분리/호출 통일 필요.
Line 212, Line 253에서 feed_score_count를 1개 인자로 호출하고 있고, item_feed.xml에서는 같은 리소스를 2개 인자로 호출합니다. 문자열 리소스 포맷이 어느 쪽이든 한쪽은 런타임에서 깨질 수 있어요. “점수만”/“점수+카운트”를 분리하거나 모든 호출을 동일 시그니처로 맞춰주세요.
Also applies to: 179-260
🤖 Prompt for AI Agents
In @app/src/main/res/layout/item_feed_detail_header.xml around lines 12 - 15,
The string resource feed_score_count is being called with different argument
counts across layouts (e.g., item_feed.xml uses two args while
item_feed_detail_header.xml uses one), which can crash at runtime; fix by
splitting the resource into two clearly named entries (e.g., feed_score_only and
feed_score_with_count) and update the data-binding expressions in both layouts
(replace usages of feed_score_count in item_feed.xml and
item_feed_detail_header.xml) to call the appropriate resource with the matching
number of arguments, or alternatively make all calls supply the same argument
list/signature if you prefer a single resource.
|
|
||
| <!-- 피드 뷰 --> | ||
| <string name="feed_score_count">%.1f (%d)</string> | ||
| <string name="feed_score_count">%.1f</string> |
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:
#!/bin/bash
# feed_score_count 문자열 리소스의 모든 사용처를 찾아 포맷 인자 개수를 확인합니다.
echo "=== feed_score_count 사용처 검색 ==="
rg -n 'feed_score_count' --type=kotlin --type=java
echo -e "\n=== 상세 컨텍스트 (앞뒤 5줄) ==="
rg -n -B 5 -A 5 'feed_score_count' --type=kotlin --type=javaRepository: Team-WSS/WSS-Android
Length of output: 120
🏁 Script executed:
#!/bin/bash
# 모든 파일 타입에서 feed_score_count를 검색합니다.
echo "=== 전체 파일에서 feed_score_count 검색 ==="
rg -n 'feed_score_count' .
echo -e "\n=== R.string.feed_score_count 검색 ==="
rg -n 'R\.string\.feed_score_count' .
echo -e "\n=== 레이아웃 파일에서 검색 ==="
fd -e xml -path '*/layout*' -o -path '*/res*' | xargs rg -l 'feed_score_count' 2>/dev/null || echo "No matches in layout files"
echo -e "\n=== strings.xml 파일 내용 확인 ==="
cat core/resource/src/main/res/values/strings.xml | grep -n 'feed_score_count' -A 2 -B 2Repository: Team-WSS/WSS-Android
Length of output: 1334
데이터 바인딩 표현식에서 포맷 인자 개수 불일치로 인한 런타임 예외 위험
문자열 포맷이 "%.1f"로 변경되어 1개의 인자만 받도록 수정되었으나, 일부 사용처에서 여전히 이전 포맷에 맞춰 2개의 인자를 전달하고 있습니다.
문제가 있는 호출:
app/src/main/res/layout/item_feed.xml:217-@string/feed_score_count(feed.novel.rating, feed.novel.ratingCount)❌ 2개 인자 전달 (1개만 필요)
올바른 호출:
app/src/main/res/layout/item_feed_detail_header.xml:212- 1개 인자 ✓app/src/main/res/layout/item_feed_detail_header.xml:253- 1개 인자 ✓
item_feed.xml의 호출을 새로운 포맷에 맞게 수정해야 하며, 그렇지 않으면 런타임 예외가 발생합니다.
🤖 Prompt for AI Agents
In @core/resource/src/main/res/values/strings.xml at line 98, The string
resource feed_score_count now expects a single float argument ("%.1f"), but one
binding still passes two args; update the data-binding expression in the
item_feed layout that currently calls
@string/feed_score_count(feed.novel.rating, feed.novel.ratingCount) to pass only
the rating (e.g., @string/feed_score_count(feed.novel.rating)) so it matches the
feed_score_count format and avoids runtime FormatException; verify other usages
already passing one arg remain unchanged.
📌𝘐𝘴𝘴𝘶𝘦𝘴
📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯
📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵
💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
스타일
✏️ Tip: You can customize this high-level summary in your review settings.