Conversation
Walkthrough앱에 Material3 의존성 추가, 검색 필터 화면·날짜 다이얼로그·품종/지역 어댑터 및 네비게이션 추가, DTO/도메인 모델 필드 확장·이름 변경, 상세 화면 3종에 지도·이미지 페이저·UI 재구성 적용, 탭 화면들을 더미데이터 기반과 새로운 리스트 어댑터로 리팩터링, 다수 리소스(레이아웃/드로어블/문자열/스타일) 추가·수정. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Search as SearchFragment
participant Nav as NavController
participant Filter as SearchFilterFragment
participant DateDlg as SearchFilterDateDialog
User->>Search: 필터 버튼 탭
Search->>Nav: action_fragment_search_to_fragment_search_filter
Nav-->>Filter: 필터 화면 표시
User->>Filter: 날짜 선택 트리거
Filter->>DateDlg: show(Type: START/END)
DateDlg-->>Filter: 선택된 날짜로 모델 업데이트
User->>Filter: 적용 버튼
Filter-->>Search: FragmentResult(SearchFilterUiModel)
sequenceDiagram
actor User
participant Detail as DetailFragment
participant MapView as NaverMapView
participant Geo as Geocoder
User->>Detail: 상세 화면 진입
Detail->>MapView: mapView.onCreate/getMapAsync
MapView-->>Detail: naverMap 준비
Detail->>Geo: 주소 -> 위도/경도 요청
alt 지오코딩 성공
Geo-->>Detail: LatLng
Detail->>MapView: 마커 추가, 카메라 이동
else 실패
Detail-->>User: Toast(주소 없음)
end
User->>Detail: 북마크 토글
Detail->>Detail: isBookmarked 갱신 → UI 업데이트
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested reviewersPoem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests
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 |
There was a problem hiding this comment.
Actionable comments posted: 32
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (1)
221-245: 무한 스크롤 로딩 가드 및 상태 관리 추가
onScrolled에서dy > 0체크 및isLoading플래그 검사(선언 필요) 후에만viewModel.getSearchReportData()호출- 호출 직전
isLoading = true설정, 데이터 수신(Observe) 시점에isLoading = false로 리셋하고lastReportId갱신- 주석 처리된
observeViewModel()활성화해 실제 응답을 구독하도록 수정app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (1)
64-67: SafeArgs 값 처리 로직 오류 및 잠재적 컴파일 타입 미스매치.ifBlank로 nullable 변수를 반환하고 있어 안전하지 않습니다. 아래처럼 명확히 처리하세요.
- cardId = args.id.ifBlank { cardId.toString() }.toLong() - tag = args.tag.ifBlank { tag } - name = args.name.ifBlank { name } + cardId = args.id.toLongOrNull() ?: -1L + tag = args.tag.takeIf { it.isNotBlank() } + name = args.name.takeIf { it.isNotBlank() }app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (1)
226-253: 페이징 중복 호출 가드 추가rvPosition == totalCount 조건만으로는 바닥에 머무를 때 연속 호출될 수 있습니다. 로딩 가드와 임계값(예: totalCount-2)을 두세요.
아래와 같이 최소 가드 추가를 권장합니다. isPaging 해제는 옵저버에서 새 데이터 수신 시점에 처리해 주세요.
- private var isNewList = false + private var isNewList = false + private var isPaging = false @@ - if (rvPosition == totalCount) { - viewModel.getSearchAllData( + if (rvPosition >= totalCount - 2 && !isPaging) { + isPaging = true + viewModel.getSearchAllData( lastProtectId, lastReportId ) }app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (1)
69-81: 데이터 로딩/옵저버 주석으로 상세 화면 공백observeViewModel()/fetchDetailData()가 비활성화되어 상세 데이터가 표시되지 않습니다. 맵도 빈 주소로 초기화됩니다(→ setupMap 재호출 필요).
-// observeViewModel() -// fetchDetailData() + observeViewModel() + fetchDetailData()데이터 수신(updateUI) 이후에 setupMap()을 재호출해 주소 기반 마커를 갱신해 주세요.
app/src/main/res/layout/fragment_search_detail_protecting.xml (1)
26-36: 북마크 버튼 접근성/인터랙션 속성 추가 필요아이콘에 contentDescription이 없고 클릭/포커스 가능 속성이 빠졌습니다. 터치 타겟도 48dp 권장치에 못 미칩니다. 최소한 아래 속성 추가를 권장합니다.
<ImageView android:id="@+id/iv_search_detail_bookmark" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:layout_marginEnd="21dp" android:layout_marginBottom="17dp" android:background="@drawable/bg_search_rounded_box" + android:minWidth="48dp" + android:minHeight="48dp" + android:clickable="true" + android:focusable="true" + android:contentDescription="@string/cd_bookmark" android:src="@drawable/ic_search_detail_blank_bookmark"app/src/main/res/layout/fragment_search_detail_disappear.xml (1)
26-38: 북마크 아이콘 a11y 속성 보완clickable/focusable 추가는 좋습니다. contentDescription과 최소 터치 영역(48dp)을 추가해 주세요.
<ImageView android:id="@+id/iv_search_detail_bookmark" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:layout_marginEnd="21dp" android:layout_marginBottom="17dp" android:background="@drawable/bg_search_rounded_box" android:clickable="true" android:focusable="true" + android:minWidth="48dp" + android:minHeight="48dp" + android:contentDescription="@string/cd_bookmark" android:src="@drawable/ic_search_detail_blank_bookmark"app/src/main/res/layout/fragment_search_detail_witness.xml (1)
26-37: 북마크 아이콘 클릭/접근성 정비현재 clickable/focusable이 없습니다. contentDescription과 함께 추가해 주세요. 상단 바 전체 아이콘(뒤로가기 포함)도 동일 기준 적용을 권장합니다.
<ImageView android:id="@+id/iv_search_detail_bookmark" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:layout_marginEnd="21dp" app:layout_constraintBottom_toBottomOf="parent" android:layout_marginBottom="17dp" android:background="@drawable/bg_search_rounded_box" + android:minWidth="48dp" + android:minHeight="48dp" + android:clickable="true" + android:focusable="true" + android:contentDescription="@string/cd_bookmark" android:src="@drawable/ic_search_detail_blank_bookmark"
🧹 Nitpick comments (114)
app/src/main/res/drawable/bg_search_filter_initial.xml (3)
5-5: 테마 속성으로 색상 치환해 다크·다이내믹 컬러 대응하세요고정색(@color/gray1) 대신 테마 속성(?attr/…)을 쓰면 Dark Mode·Material You에 자연스럽게 녹아듭니다. 보통 배경은 colorSurfaceVariant/SecondaryContainer가 무난합니다.
- <solid android:color="@color/gray1"/> + <solid android:color="?attr/colorSurfaceVariant"/>만약 브랜드 고정 그레이가 의도라면 night/colors.xml에도 동일 키를 정의했는지 확인 부탁드려요.
6-6: 라운드 값은 @Dimen으로 추출해 재사용성↑직접 dp 값 대신 dimen 리소스로 빼두면 다른 필터 배경들과 일관성 유지가 쉽습니다.
- <corners android:radius="30dp"/> + <corners android:radius="@dimen/search_filter_corner_radius"/>추가: res/values/dimens.xml
<dimen name="search_filter_corner_radius">30dp</dimen>
2-8: 상태(pressed/disabled/selected) 피드백 필요 시 selector/ripple 고려현재는 단색 배경이라 상호작용 피드백이 없습니다. 이 리소스가 클릭 가능한 필터의 “초기/미선택” 배경이라면 state selector 또는 RippleDrawable을 통해 터치/선택 상태를 분리하는 걸 권장합니다. Material3 Chip/MaterialButton을 쓰면 기본 ripple·shapeAppearance도 공짜로 따라옵니다.
예시: res/drawable/bg_search_filter_initial_selector.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="false"> <shape><solid android:color="?attr/colorSurfaceContainerLowest"/></shape> </item> <item android:state_pressed="true"> <shape><solid android:color="?attr/colorSurfaceVariant"/></shape> </item> <item android:state_selected="true"> <shape><solid android:color="?attr/colorSecondaryContainer"/></shape> </item> <item> <shape><solid android:color="?attr/colorSurfaceContainerLow"/></shape> </item> </selector>추가로, 이번 PR에 포함된 다른 필터 배경(drawable/bg_search_filter_button_*.xml)들과 색/라운드 값이 일관한지도 한 번만 체크 부탁드려요.
app/src/main/res/drawable/bg_search_detail_bottom.xml (1)
16-16: 반복되는 23dp 라운드 값 dimen으로 추출 권장유지보수/일관성을 위해 반경을 dimen으로 빼두면 좋습니다. 그라데이션 아이템의 코너에도 동일 값 재사용 바랍니다.
- <corners android:topLeftRadius="23dp" android:topRightRadius="23dp" /> + <corners + android:topLeftRadius="@dimen/shape_corner_23dp" + android:topRightRadius="@dimen/shape_corner_23dp" />res/values/dimens.xml 추가 예:
<resources> <dimen name="shape_corner_23dp">23dp</dimen> </resources>app/src/main/res/layout/fragment_search_rescue.xml (3)
9-17: 배너 접근성/레이아웃 개선 제안
- 배너가 장식용이면 contentDescription을 null 처리하고 접근성 제외.
- 가로는 부모폭에 맞추고 centerCrop 적용이 안전합니다.
다음과 같이 수정 권장:
- android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="0dp" + android:layout_height="wrap_content" android:layout_marginTop="15dp" android:src="@drawable/img_search_banner_adopt" + android:scaleType="centerCrop" + android:adjustViewBounds="true" + android:contentDescription="@null" + android:importantForAccessibility="no" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
9-37: 테마 일관성(아이콘 틴트) 제안다크모드/테마 변경 대응을 위해 아이콘에 틴트 속성 적용을 고려하세요.
- android:src="@drawable/img_search_filters" + android:src="@drawable/img_search_filters" + app:tint="?attr/colorOnSurface"동일하게 정렬 버튼에도 적용 가능.
13-34: 하드코딩 마진 15dp 다수공통 여백은 @Dimen로 추출해 재사용하면 유지보수가 쉬워집니다.
- android:layout_marginTop="15dp" + android:layout_marginTop="@dimen/spacing_m"app/src/main/res/drawable/bg_search_detail_special_note.xml (1)
1-4: 색상 하드코딩을 테마/리소스로 전환해 다크모드 대응하세요
배경 색을 고정 HEX 대신 테마/리소스로 두면 일관성과 다크모드 대응이 좋아집니다.아래처럼 교체 제안:
- <solid android:color="#F2F4F6"/> + <solid android:color="?attr/colorSurfaceVariant"/>추가로, 다른 배경들과 모서리 라운드가 필요하면 코너 반경을 맞춰주는 것도 고려해 주세요.
app/src/main/res/drawable/ic_search_calendar.xml (1)
1-5: 아이콘 컬러를 공용 리소스/테마로 일원화
벡터 내부에 HEX 틴트를 두기보다 공용 컬러 사용을 권장합니다(다크모드/테마 일관성).-<vector ... android:tint="#A0A0A0" ...> +<vector ... android:tint="@color/gray4" ...>또는 벡터에서 tint를 제거하고, 사용처(ImageView)에서 tint를 적용하는 방식도 유지보수에 유리합니다.
app/src/main/res/drawable/ic_search_grid_sort.xml (1)
8-17: 아이콘 내부 컬러 혼재 정리(검정 → 공용 회색)
일부 path는 여전히#111111, 일부는@color/gray4로 섞여 있어 테마 일관성이 깨질 수 있습니다. 초기 4개 path의 fillColor도@color/gray4로 통일을 권장합니다.- android:fillColor="#111111"/> + android:fillColor="@color/gray4"/> - android:fillColor="#111111"/> + android:fillColor="@color/gray4"/> - android:fillColor="#111111"/> + android:fillColor="@color/gray4"/> - android:fillColor="#111111"/> + android:fillColor="@color/gray4"/>대안: 벡터 최상단에
android:tint="@color/gray4"를 두고 모든 path를 단색(흰색)으로 두어 유지보수를 단순화하는 방법도 있습니다.Also applies to: 22-23, 29-30, 36-37, 43-44
app/src/main/res/drawable/ic_search_filter_close.xml (1)
9-10: 하드코딩 색상 제거 + 불필요한 fill 제거 제안
- 폐쇄되지 않은 선(path)이라 fillColor는 의미가 없고, 다크/라이트 테마 대응을 위해 하드코딩(#111111) 대신 테마/리소스를 쓰는 게 좋아요.
아래처럼 정리 어떨까요?
- android:fillColor="#111111" - android:strokeColor="#111111" + android:fillColor="@android:color/transparent" + android:strokeColor="?attr/colorOnSurface"app/src/main/res/drawable/ic_search_map.xml (1)
6-12: 아이콘 컬러 테마화현재 #FF9000이 하드코딩되어 있어 테마 전환(다크모드 등)과 상태 색상 일관성이 떨어질 수 있어요. 프로젝트에서 사용하는 메인 컬러 리소스 혹은 테마 속성으로 대체를 권장합니다.
예:
- android:strokeColor="#FF9000" + android:strokeColor="@color/main_color"또는 공통 틴트 사용 시(레이아웃/코드에서 tint 적용): strokeColor를 흰색으로 두고 외부에서 tint만 바꾸는 패턴도 고려해보세요.
app/src/main/res/drawable/bg_search_filter_button_right.xml (2)
6-11: 체크/디폴트 상태의 코너 반경 불일치 확인 필요checked(8dp) vs default(30dp)로 우측 버튼의 코너 반경이 달라 시각적 단차가 생길 수 있어요. 의도라면 OK, 아니라면 반경을 맞춰주세요.
예:
- <corners android:bottomRightRadius="8dp" android:topRightRadius="8dp" /> + <corners android:bottomRightRadius="30dp" android:topRightRadius="30dp" />
14-18: 눌림/포커스 피드백 추가 제안상태가 checked와 default만 있어 터치 피드백이 약해요. ripple 또는 state_pressed/ state_focused 항목을 추가하면 UX가 좋아집니다.
예(머티리얼 권장 ripple):
<ripple android:color="?attr/colorControlHighlight"> <item> <!-- 현재 shape를 그대로 이동 --> </item> </ripple>app/src/main/res/drawable/bg_search_filter_picker_selection.xml (1)
1-5: 색상/치수 하드코딩 제거
- 색상 #f2f4f6, 반경 8dp가 하드코딩되어 다크모드 및 디자인 스케일 변경에 취약합니다. @color/@Dimen 리소스로 치환 권장.
예:
- <solid android:color="#f2f4f6"/> - <corners android:radius="8dp"/> + <solid android:color="@color/gray1"/> + <corners android:radius="@dimen/radius_8"/>또한 필요 시 values-night에 대응 색상 추가를 고려해주세요.
app/src/main/res/drawable/search_detail_phone.xml (1)
6-12: 아이콘 컬러 테마화 + stroke 전용이면 fill 투명화
- #FFA938 하드코딩 대신 @color/main_color 또는 테마 속성 사용을 권장합니다.
- 현재 path가 채워진 실루엣이라면 OK지만, 선형 아이콘 의도라면 fill을 투명으로 두는 편이 더 깔끔합니다.
예:
- android:fillColor="#FFA938" - android:strokeColor="#FFA938" + android:fillColor="@android:color/transparent" + android:strokeColor="@color/main_color"또는 외부 tint 전략을 쓸 경우 strokeColor를 고정(흰색 등)하고 뷰에서 tint로 일괄 제어하는 패턴도 고려해보세요.
app/src/main/res/drawable/bg_search_map_round.xml (1)
4-5: 다크 테마 대응: 하드코드 색/반경을 테마·리소스로 치환 권장화이트 고정은 다크모드에서 튀고, 15dp 하드코드도 재사용이 어렵습니다. 테마/디멘션 리소스로 바꾸면 유지보수성이 좋아집니다.
- <corners android:radius="15dp"/> - <solid android:color="@android:color/white"/> + <corners android:radius="@dimen/search_corner_radius"/> + <solid android:color="?attr/colorSurface"/>(참고: @dimen/search_corner_radius 가 없다면 추가해주세요.)
app/src/main/res/drawable/ic_search_detail_blank_bookmark.xml (1)
7-12: 아이콘 색상 하드코딩 제거 → 테마 속성 사용strokeColor #111111 대신 테마 컬러를 참조하면 다크모드/동적 색상에 자연스럽게 대응됩니다.
- android:strokeColor="#111111" + android:strokeColor="?attr/colorOnSurface"필요 시 눌림/선택 상태는 ColorStateList(@color/selector_icon_bookmark 등)로 확장해 주세요.
app/src/main/res/drawable/ic_search_filter_drop.xml (2)
2-3: 종횡비 불일치 가능성 (왜곡 위험)viewport 9x7인데 size 10dp x 4.56dp로 지정되어 비율이 달라 왜곡될 수 있습니다. 비율(9:7)을 유지하도록 height를 보정하거나 width를 원래대로 두는 것을 권장합니다.
- android:width="10dp" - android:height="4.56dp" + android:width="10dp" + android:height="7.78dp" <!-- 10 * (7/9) -->
11-12: 중성 회색 하드코드 대신 M3 컬러 토큰 사용#A0A0A0 고정값보다 Material3 권장 속성 사용이 일관됩니다(예: colorOutline/onSurfaceVariant).
- android:strokeColor="#A0A0A0" + android:strokeColor="?attr/colorOutline"app/src/main/res/drawable/ic_search_phone.xml (2)
1-5: 브랜드 컬러를 벡터에 직접 고정하기보다 tint로 제어 권장아이콘 공통 스타일을 위해 벡터에 tint를 두고 path는 중립색(흰색)으로 두면 테마/상태별 재사용이 쉽습니다.
-<vector xmlns:android="http://schemas.android.com/apk/res/android" +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="18dp" android:height="18dp" android:viewportWidth="18" - android:viewportHeight="18"> + android:viewportHeight="18" + android:tint="?attr/colorPrimary">
10-11: fill+stroke 브랜드색 동시 지정 → 중립색 + tint로 단순화중복 지정은 두께/가독성 이슈를 만들 수 있습니다. tint 적용을 전제로 path 색은 중립으로 놓는 편이 안전합니다.
- android:fillColor="#FFA938" - android:strokeColor="#FFA938" + android:fillColor="#FFFFFFFF" + android:strokeColor="#FFFFFFFF"배경 대비가 낮을 수 있으니 실제 배치 배경과의 명도 대비도 한번 확인 부탁드려요.
app/src/main/java/com/example/findu/presentation/ui/search/model/SearchDate.kt (1)
3-3: Type → SearchDateType으로 변경 및 참조 업데이트
app/src/main/java/com/example/findu/presentation/ui/search/model/SearchDate.kt의-enum class Type { DATE_START, DATE_END } +enum class SearchDateType { DATE_START, DATE_END }아래 파일들에서
Type.DATE_START/Type.DATE_END를SearchDateType.DATE_START/SearchDateType.DATE_END로 모두 수정하세요.
- app/src/main/java/com/example/findu/presentation/ui/search/dialog/SearchFilterDateDialog.kt (라인 55, 86, 117)
- app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt (라인 277, 298)
app/src/main/res/layout/item_search_breeds.xml (2)
20-21: LinearLayout 안에서 Constraint 속성은 무시됩니다 — 제거 또는 부모 통일 권장
app:layout_constraint*속성은 해당 뷰들이LinearLayout하위에 있을 때 적용되지 않습니다. 혼선을 줄이기 위해 제거하거나, 필요하다면 부모를ConstraintLayout으로 통일하세요.제안 diff:
- app:layout_constraintStart_toStartOf="parent" /> + /> ... - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tv_breed_name" /> + />Also applies to: 28-29
7-22: 터치 타겟 확보 제안 (접근성·사용성)리스트 아이템 최소 높이(48dp) 보장을 위해 상위 컨테이너에
android:minHeight="48dp"추가를 검토해 주세요.app/src/main/res/drawable/ic_search_horizontal_sort.xml (1)
8-11: 중복 path로 인한 불필요 오버드로우 제거동일한
pathData가 두 번씩 그려집니다(첫 번째와 세 번째, 두 번째와 네 번째). 시각적 차이 없이 오버드로우만 늘어나므로 하나로 정리하세요.제안 diff(3·4번째 path만 유지):
- <path - android:pathData="M19.875,17.792C19.875,18.942 18.955,19.875 17.82,19.875L3.18,19.875C2.045,19.875 1.125,18.942 1.125,17.792L1.125,14.667C1.125,13.516 2.045,12.583 3.18,12.583L17.82,12.583C18.955,12.583 19.875,13.516 19.875,14.667V17.792Z" - android:fillColor="@color/gray4"/> ... - <path - android:pathData="M19.875,3.208C19.875,2.058 18.955,1.125 17.82,1.125L3.18,1.125C2.045,1.125 1.125,2.058 1.125,3.208L1.125,6.333C1.125,7.484 2.045,8.417 3.18,8.417H17.82C18.955,8.417 19.875,7.484 19.875,6.333V3.208Z" - android:fillColor="@color/gray4"/>추가 제안: 루트에
android:tint를 사용하면 테마 변경이 쉬워집니다(선택).Also applies to: 16-18, 23-25
app/src/main/res/drawable/ic_search_detail_back.xml (1)
1-5: 하드코딩 색상 → 리소스화 및 RTL 자동 미러링 권장다른 아이콘들과 일관되게 색상을 리소스로 전환하고, Back 아이콘 특성상 RTL에서 자동 반전되도록 설정해 주세요.
제안 diff:
-<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" android:height="16dp" android:viewportWidth="18" - android:viewportHeight="16"> + android:viewportHeight="16" + android:autoMirrored="true"> ... - android:strokeColor="#111111" + android:strokeColor="@color/gray4"Also applies to: 11-11
app/src/main/res/drawable/bg_search_filter_button_left.xml (1)
6-9: 세그먼트 좌/중/우 배경이 동일 Pill이면 리소스 통합 고려
left전용 이름이지만 모서리를 전부 30dp로 통일하여 좌/중/우가 동일해졌다면 하나의 셀렉터로 합칠 수 있습니다(유지보수 감소). 반대로 진짜 세그먼트 형태(맞닿는 모서리 0dp)를 원한다면 middle/right도 확인이 필요합니다.UI 스크린샷 기준으로 인접 버튼 간 경계/라운드가 기대대로 보이는지 한 번 더 확인 부탁드립니다(특히 체크 상태의 2dp 스트로크가 중앙 결합부에서 이중으로 보이지 않는지).
Also applies to: 14-15
app/src/main/res/drawable/ic_search_blank_bookmark.xml (2)
10-12: 하드코딩된 흰색 → 테마 대응 색상으로 전환 제안다크모드/테마 전환 시 명암비/일관성 이슈가 있어요. 벡터 루트 tint를 사용하고 path의 색상은 테마 무관 기본값으로 두는 편이 안전합니다.
아래처럼 path 색을 비테마 기본값으로 낮추고(투명/검정), 루트에 tint를 추가해 주세요.
- android:fillColor="#ffffff" - android:fillAlpha="0.5" - android:strokeColor="#ffffff" + android:fillColor="@android:color/transparent" + android:strokeColor="@android:color/black"추가(라인 1~5 외부 변경 예시):
<vector ... android:tint="?attr/colorOnSurfaceVariant">다크/라이트에서 시인성 확인 부탁드립니다.
9-9: strokeWidth 2dp 시각적 두께 확인아이콘 세트 대비 두껍게 보일 수 있어요. 1.5dp 또는 디자인 스펙 재확인 권장.
app/src/main/java/com/example/findu/domain/model/search/DetailReportData.kt (2)
14-15: 비nullable 필드 기본값 권장(역호환/안전성)새 필드가 비nullable인데 기본값이 없어 이전 호출부/시리얼라이즈 복원 시 NPE 리스크가 있습니다. 기본값 추가를 권장합니다.
- val userPhone : String, - val surroundPlace : String, + val userPhone : String = "", + val surroundPlace : String = "",
10-11: age/rfid 타입 정교화 고려age, rfid가 String?인데, 도메인 의미상 Int?/Value Object로의 정교화가 유지보수에 유리합니다(검증/포맷 분리).
app/src/main/java/com/example/findu/presentation/ui/search/model/DummySearchData.kt (1)
3-3: 날짜 포맷 앱 전반과 일치(yyyy.MM.dd) 제안UI 리소스가 점(.) 포맷을 사용 중입니다. toString() 대신 포맷 명시를 권장.
-import java.time.LocalDate +import java.time.LocalDate +import java.time.format.DateTimeFormatter @@ - private fun today() = LocalDate.now().toString() + private val fmt = DateTimeFormatter.ofPattern("yyyy.MM.dd") + private fun today() = LocalDate.now().format(fmt)Also applies to: 6-6
app/src/main/res/layout/fragment_search_all.xml (4)
10-19: 배너 ImageView 접근성/레이아웃 개선
- contentDescription 누락 → 보조공학 노출 불가
- wrap_content 폭은 기기별 들쑥날쑥. 가로 꽉 채우기 권장
<ImageView android:id="@+id/iv_search_banner" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="0dp" + android:layout_height="wrap_content" android:layout_marginTop="15dp" android:src="@drawable/img_search_banner_adopt" + android:adjustViewBounds="true" + android:scaleType="centerCrop" + android:contentDescription="@string/home_banner" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
21-29: 필터 버튼 접근성/터치 타겟/리플 적용
- contentDescription 없음
- background @null → 리플 제거
- 시작 constraint를 배너 대신 parent에 거는 편이 안전
<ImageButton android:id="@+id/ib_search_filter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="15dp" - android:background="@null" + android:background="?attr/selectableItemBackgroundBorderless" android:src="@drawable/img_search_filters" - app:layout_constraintStart_toStartOf="@id/iv_search_banner" + android:contentDescription="@string/search_filter_type" + android:minWidth="48dp" + android:minHeight="48dp" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/iv_search_banner" />
30-38: 정렬 버튼 접근성/터치 타겟/리플 적용36dp는 권장 최소 48dp에 미달. contentDescription/리플 추가 권장.
<ImageButton android:id="@+id/ib_search_horizontal_sort" - android:layout_width="36dp" - android:layout_height="36dp" + android:layout_width="48dp" + android:layout_height="48dp" android:layout_marginTop="15dp" - android:background="@null" + android:background="?attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_search_horizontal_sort" + android:contentDescription="@string/search_see_more" app:layout_constraintEnd_toEndOf="@id/iv_search_banner" app:layout_constraintTop_toBottomOf="@id/iv_search_banner" />
41-49: RecyclerView 폭 고정(320dp) 대신 매칭 권장화면별 반응형을 위해 width 0dp(매칭 제약) 권장. 현재 라인 범위 밖이라 참고 코드로 남깁니다.
참고:
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_search_horizontal_content" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/ib_search_filter" app:layout_constraintBottom_toBottomOf="parent" tools:listitem="@layout/search_horizontal_content_item"/>app/src/main/res/values/strings.xml (2)
66-67: 정적 날짜 값 대신 런타임 포맷 사용 권장"2024.11.04" 하드코딩은 현행성/현지화 이슈가 있습니다. date_single/date_range 포맷 리소스를 사용해 코드에서 포맷팅해 주세요.
96-96: 표현 통일(한글화) 제안: 'Y' → '예'한국어 UI에서 'Y'는 튑니다. '예/아니오' 또는 '중성화 완료/미완료' 등 명확한 표현을 권장.
- <string name="search_detail_neuter_value">Y</string> + <string name="search_detail_neuter_value">예</string>Also applies to: 102-102
app/src/main/res/layout/item_search_location.xml (1)
2-2: ConstraintLayout 속성이 남아있어요 — LinearLayout에선 무효입니다. 정리 부탁!
현재 TextView와 View에 app:layout_constraint* 속성이 남아있는데, 부모가 LinearLayout이라 아무 효과가 없습니다. 네임스페이스도 함께 제거해 주세요. 또 내부 여백은 컨테이너에 padding으로 주고, TextView margin은 줄이는 게 재사용/터치 타겟(48dp 이상) 측면에서 좋아요.적용 예시:
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> ... - <LinearLayout + <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:paddingVertical="12dp" + android:minHeight="48dp"> ... - <TextView + <TextView android:id="@+id/tv_location_name" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginVertical="12dp" - android:layout_marginStart="16dp" - android:gravity="center_vertical" + android:gravity="center_vertical" android:textAppearance="@style/TextAppearance.FindU.Body2_R_14" android:textColor="@color/gray6" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + /> ... - <View + <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/gray2" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tv_location_name" /> + />Also applies to: 15-21, 25-30
app/src/main/res/drawable/bg_search_filter_check.xml (1)
5-7: 터치 피드백(ripple) 부재 — 확인 버튼에 누름 상태를 추가해 주세요
현 배경은 단색 shape만 있어 눌림 피드백이 없습니다. 뷰에 foreground로 ripple을 주거나, 배경을 ripple로 감싸는 걸 추천합니다. 패딩은 레이아웃/스타일에서 관리하는 편이 유지보수에 유리합니다.배경을 ripple로 감싸는 예시:
-<?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - - <solid android:color="@color/main_color"/> - <corners android:radius="30dp"/> - -</shape> +<?xml version="1.0" encoding="utf-8"?> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?attr/colorControlHighlight"> + <item> + <shape android:shape="rectangle"> + <solid android:color="@color/main_color"/> + <corners android:radius="30dp"/> + </shape> + </item> +</ripple>대안: 레이아웃 쪽에 android:foreground="?attr/selectableItemBackground"를 지정.
app/build.gradle.kts (1)
66-99: Material 중복 의존성 정리 필요 (버전 충돌/리소스 충돌 리스크)
이미 libs.material이 있는데, 1.9.0을 직접 추가해 중복됩니다. Material3(androidx.material3:material3-android) 추가 자체는 OK. 중복을 제거해 의존성 해상도를 단순화해 주세요.권장 변경:
implementation(libs.material) ... -implementation("com.google.android.material:material:1.9.0")검증 명령(로컬 실행 권장):
- ./gradlew :app:dependencies --configuration debugRuntimeClasspath | rg -n "material"
- ./gradlew :app:lintDebug
추가로 viewpager2는 안정판(1.1.0 이상)으로 올리는 것도 고려해 주세요.app/src/main/res/drawable/ic_search_map_marker.xml (1)
7-16: 하드코딩 색상 → 리소스 참조로 교체 권장 (다크모드/테마 대응)
#FF9000, #ffffff가 하드코딩되어 있어 테마 전환 시 유연성이 떨어집니다. 팔레트 색(@color/…)로 치환을 추천합니다.예시:
- android:fillColor="#FF9000" + android:fillColor="@color/main_color" ... - android:fillColor="#ffffff" + android:fillColor="@color/white" ... - android:fillColor="#ffffff"/> + android:fillColor="@color/white"/>참고: 두 톤 컬러를 유지해야 해서 tint 한 번으로는 대체가 어렵습니다. 다크 테마용 night 리소스도 함께 준비해 주세요.
app/src/main/res/drawable/bg_search_filter_button_middle.xml (1)
3-18: pressed/disabled 상태 및 Ripple 누락 — 상호작용 피드백 보강 제안
checked/unchecked만 있고 pressed/disabled 상태, ripple이 없습니다. 실제 사용 뷰에 android:foreground="?attr/selectableItemBackground" 추가를 권장합니다. 또한 세그먼트 “middle”도 30dp 라운드면 인접 버튼과 경계가 모호할 수 있습니다. UI 가이드에 맞는 간격/분리 여부 확인 부탁드려요.체크리스트:
- 레이아웃(예: fragment_search_filter.xml)의 해당 버튼에 foreground ripple 적용 여부
- disabled 상태(예: state_enabled="false") 색상 대비
- 세그먼트 간 경계(간격/스트로크) 시인성
app/src/main/res/drawable/ic_search_message.xml (1)
6-12: 하드코드 컬러 대신 리소스/테마 참조 권장다크모드/테마 대응을 위해 strokeColor를 하드코드(#FF9000)보다 @color 또는 ?attr 기반으로 두는 게 안전합니다.
적용 예시:
- android:strokeColor="#FF9000" + android:strokeColor="@color/main_color"gradle/libs.versions.toml (1)
30-30: Compose BOM과 Material3 버전 정렬현재 composeBom(2024.04.01)과 material3-android(1.3.2)를 혼용 버전 관리 중입니다. 드리프트 방지를 위해:
- 옵션 A: BOM에 맡기고 M3는 버전 제거(가능하면
material3아티팩트 사용).- 옵션 B: 현재처럼 고정 버전을 유지하되, BOM과의 호환성(컴파일러/Kotlin 1.9.24) 빌드로 확인.
옵션 A 예시:
- material3Android = "1.3.2" ... - androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" } + # (버전은 BOM 관리, 아티팩트는 material3 권장) + androidx-material3-android = { module = "androidx.compose.material3:material3" }Also applies to: 77-77
app/src/main/res/layout/item_search_breed_chip.xml (2)
3-11: close 아이콘 가시성/접근성 속성 보강 제안스타일에서 제어하지 않는다면 아래 속성 추가를 권장합니다. 접근성 라벨도 함께 두면 좋아요.
app:closeIcon="@drawable/ic_report_dialog_close_10" app:closeIconTint="@color/main_color" + app:closeIconVisible="true" + app:closeIconContentDescription="@string/content_desc_remove_breed"
7-7: placeholder 텍스트 제거 권장런타임에 바인딩된다면 기본값 "test"는 지우는 게 안전합니다(노출 리스크).
- android:text="test" + android:text="@null"app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchFilterLocationRVAdapter.kt (3)
30-34: 선택 변경 시 전체 갱신 대신 변경 항목만 갱신notifyDataSetChanged()는 과합니다. 이전/새 인덱스만 부분 갱신하면 깜빡임 줄고 성능이 좋아집니다.
@SuppressLint("NotifyDataSetChanged") fun updateSelected(newSelected: String?) { - selectedLocation = newSelected - notifyDataSetChanged() + if (selectedLocation == newSelected) return + val oldIndex = locations.indexOf(selectedLocation) + val newIndex = locations.indexOf(newSelected) + selectedLocation = newSelected + if (oldIndex != -1) notifyItemChanged(oldIndex) + if (newIndex != -1) notifyItemChanged(newIndex) }
44-52: 중복 setTextColor 호출 제거isSelected 분기에서 main_color를 두 번 설정합니다. 한 번만 유지하세요.
if (isSelected) { - tvLocationName.setTextColor( - ContextCompat.getColor( - root.context, - R.color.main_color - ) - ) tvLocationName.setTextAppearance(R.style.TextAppearance_FindU_Body2_SB_14) tvLocationName.setTextColor(ContextCompat.getColor(root.context, R.color.main_color)) } else {
39-39: 불필요한 @SuppressLint 제거 및 클릭 핸들러도 부분 갱신bind()의 @SuppressLint는 불필요합니다. 클릭 시도 전체 갱신 대신 변경 항목만 갱신으로 통일하면 일관성 좋아요.
- @SuppressLint("NotifyDataSetChanged") fun bind(location: String, isSelected: Boolean) {root.setOnClickListener { if (selectedLocation != location) { - selectedLocation = location - onLocationSelected(location) - notifyDataSetChanged() + val oldIndex = locations.indexOf(selectedLocation) + selectedLocation = location + onLocationSelected(location) + if (oldIndex != -1) notifyItemChanged(oldIndex) + notifyItemChanged(bindingAdapterPosition) } }Also applies to: 56-61
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchDetailVPAdapter.kt (3)
10-11: List는 타입 안전성이 떨어져요 — 전용 이미지 모델(Sealed class)로 교체 제안URL/리소스 두 타입을 다루는 목적이면 제네릭/Sealed 모델로 명시적으로 제한하는 게 안전합니다. 실수로 지원하지 않는 타입이 들어와도 컴파일 타임에 잡을 수 있어요.
선택지 A) 최소 변경 diff:
-class SearchDetailVPAdapter(private val images:List<Any>) : +class SearchDetailVPAdapter(private val images: List<SearchImageItem>) :선택지 A를 쓰는 경우, 아래 보조 코드 추가:
// 같은 파일 상단 또는 별도 파일 sealed class SearchImageItem { data class Url(val url: String) : SearchImageItem() data class Res(val resId: Int) : SearchImageItem() }
15-25: Glide 대상 View에 바인딩 + 타입 통일 + 가드 추가
- Glide.with(binding.ivSearchDetailContent)로 View lifecycle에 안전하게 묶어요.
- Int도 Glide로 통일하면 재활용 시 일관성↑.
- 예외 타입은 가드 처리.
- fun bind(item: Any) { - when (item) { - is String -> { - Glide.with(binding.root) - .load(item) - .into(binding.ivSearchDetailContent) - } - is Int -> { - binding.ivSearchDetailContent.setImageResource(item) - } - } - } + fun bind(item: Any) { + when (item) { + is String -> { + Glide.with(binding.ivSearchDetailContent) + .load(item) + .into(binding.ivSearchDetailContent) + } + is Int -> { + Glide.with(binding.ivSearchDetailContent) + .load(item) + .into(binding.ivSearchDetailContent) + } + else -> { + // unsupported type guard + binding.ivSearchDetailContent.setImageDrawable(null) + } + } + }추가 팁(선택): placeholder/error, scaleType(centerCrop) 지정하면 UX 안정적입니다.
36-42: 재활용 시 이미지 깜빡임 방지: clear 호출 권장RecyclerView 재활용에서 이전 요청이 남아 있을 수 있어요. onViewRecycled에서 clear 해주세요.
// Adapter 내부에 추가 (선택) override fun onViewRecycled(holder: ViewHolder) { Glide.with(holder.itemView).clear(holder.itemView.findViewById(R.id.ivSearchDetailContent)) super.onViewRecycled(holder) }app/src/main/res/navigation/nav_graph.xml (2)
53-56: 필터 화면 액션 추가 좋습니다. 전환 애니메이션은 추후에 넣어보세요현재 네비게이션 흐름 문제 없어 보여요. UX를 위해 enter/exit 애니메이션 속성(app:enterAnim 등) 추가를 고려하면 더 매끈합니다.
176-181: 하드코딩 라벨 → 문자열 리소스로 이동 권장i18n/일관성 측면에서 label을 strings.xml로 빼는 게 좋아요.
- android:label="Search Filter" + android:label="@string/search_filter_title"보조: strings.xml
<string name="search_filter_title">Search Filter</string>app/src/main/java/com/example/findu/data/mapper/todomain/DetailSearchResponseDtoMapper.kt (2)
42-50: 하드코딩된 기본값 문자열(“정보 없음”)은 레이어 침투 위험 — 상수/리소스로 분리 권장데이터/도메인 레이어에 한글 UI 문자열이 박히면 i18n과 테스트에 불리합니다. 선택지:
- 도메인은 null 유지, 프리젠테이션에서 표시 문자열로 대체
- 또는 공용 상수(예: Defaults.NO_INFO)로 통일
52-58: 알 수 없는 태그로 크래시 위험 — UNKNOWN으로 폴백 권장서버 측 태그가 확장되면 예외로 앱이 죽습니다. UNKNOWN으로 매핑하면 안전해요.
fun String.toDetailSearchStatus(): SearchStatus { return when (this) { "보호중" -> SearchStatus.PROTECTING "목격신고" -> SearchStatus.WITNESS "실종신고" -> SearchStatus.MISSING - else -> throw IllegalArgumentException("Unknown tag value: $this") + else -> SearchStatus.UNKNOWN } }app/src/main/res/layout/dialog_search_filter_date.xml (1)
18-24: 320dp 고정 폭 + 절대 마진은 기기별 레이아웃 깨짐 유발현재 선택 하이라이트/마스크가 좌측 정렬되어 작은 화면에서 어긋날 수 있습니다. 최소한 가로 중앙 정렬을 보장해주세요. (이상적으로는 DatePicker에 상대 배치)
샘플 수정(가로 중앙 정렬):
<FrameLayout android:layout_width="320dp" android:layout_height="36dp" android:layout_marginTop="170dp" android:background="@drawable/bg_search_filter_picker_selection" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> ... <FrameLayout android:id="@+id/fl_top_mask" android:layout_width="320dp" android:layout_height="36dp" android:layout_marginTop="110dp" android:background="@color/white" android:visibility="invisible" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> ... <FrameLayout android:id="@+id/fl_bottom_mask" android:layout_width="320dp" android:layout_height="36dp" android:layout_marginTop="230dp" android:background="@color/white" android:visibility="invisible" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" />권장 확인:
- 소형(360dp), 중형(411dp), 대형 폭 기기에서 오버레이 정렬 상태 확인
- 폰트 스케일 1.3x 이상에서 겹침 여부
Also applies to: 81-99
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchBreedRVAdapter.kt (4)
47-50: 클릭 시 전체 갱신 대신 부분 갱신 사용선택 토글은 보통 한 아이템만 변하므로 부분 갱신이 효율적입니다.
root.setOnClickListener { onPick(text) - notifyDataSetChanged() + notifyItemChanged(bindingAdapterPosition) }
56-58: 로케일 독립 소문자 비교로 안정성 향상케이스 폴딩은 로케일에 따라 달라질 수 있어 검색 일관성이 깨질 수 있습니다. Locale.ROOT 사용을 권장합니다.
- val q = cs?.toString()?.trim()?.lowercase().orEmpty() - val list = if (q.isEmpty()) allItems else allItems.filter { it.lowercase().contains(q) } + val q = cs?.toString()?.trim()?.lowercase(Locale.ROOT).orEmpty() + val list = if (q.isEmpty()) allItems else allItems.filter { it.lowercase(Locale.ROOT).contains(q) }추가:
import java.util.Locale
40-45: TextView#setTextAppearance 호환성minSdk < 23 환경이라면 TextViewCompat 사용이 안전합니다. 프로젝트 minSdk에 따라 선택해주세요.
- tvBreedName.setTextAppearance(R.style.TextAppearance_FindU_Body2_SB_14) + TextViewCompat.setTextAppearance(tvBreedName, R.style.TextAppearance_FindU_Body2_SB_14) ... - tvBreedName.setTextAppearance(R.style.TextAppearance_FindU_Body2_R_14) + TextViewCompat.setTextAppearance(tvBreedName, R.style.TextAppearance_FindU_Body2_R_14)import androidx.core.widget.TextViewCompat
19-20: 대량 데이터 대비 DiffUtil/ListAdapter 고려필터링과 선택 갱신이 잦다면 ListAdapter + DiffUtil로 jank를 줄일 수 있습니다. 현재 규모가 작으면 현 상태도 OK.
Also applies to: 64-66, 69-72
app/src/main/res/values/styles.xml (3)
80-86: BottomSheet 테마를 Material3로 일원화프로젝트에서 M3를 도입했다면 M2/Design 혼용을 피하는 것이 안전합니다.
- <style name="searchFilterBaseBottomSheetDialog" parent="@style/Theme.Design.Light.BottomSheetDialog"> + <style name="searchFilterBaseBottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog"> <item name="android:windowIsFloating">false</item> <item name="bottomSheetStyle">@style/searchFilterBottomSheet</item> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:backgroundDimEnabled">true</item> <item name="android:backgroundDimAmount">0.6</item> </style>
90-97: DatePicker 스타일 속성 유효성 점검 필요spinner 모드에서
colorControlNormal,android:divider가 기기/OS별로 무시되는 경우가 많습니다(특히 NumberPicker 내부). 현재는 오버레이(선택 하이라이트)로 보완하고 있지만, 실제 기기에서 일관성 확인이 필요합니다.권장 확인:
- API 26/29/33/35 실기/에뮬레이터에서 divider 적용 여부
- 삼성/샤오미 등 커스텀 스킨에서 표시 이상 여부
대안:
- 현재처럼 오버레이 방식 유지(권장)
- 또는 theme에 numberPickerStyle 정의 후 selectionDivider 지정(기기별 편차 있음)
29-36: Shape overlay 스타일 parent 명시shapeAppearanceOverlay에 넘길 스타일은 관련 오버레이 parent를 지정하면 더 안정적입니다.
- <style name="RoundedChipShape" parent=""> + <style name="RoundedChipShape" parent="ShapeAppearanceOverlay.MaterialComponents"> <item name="cornerFamily">rounded</item> <item name="cornerSize">50%</item> </style> - <style name="RoundedChipShape30" parent=""> + <style name="RoundedChipShape30" parent="ShapeAppearanceOverlay.MaterialComponents"> <item name="cornerFamily">rounded</item> <item name="cornerSize">30%</item> </style>app/src/main/java/com/example/findu/presentation/ui/search/dialog/SearchFilterDateDialog.kt (1)
112-125: 중복 가시성 제어 제거isVisible로 이미 GONE/VISIBLE을 설정하고 있어 수동 visibility 변경은 중복입니다. 위 패치에 포함해 제거했습니다.
app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt (5)
131-141: TextWatcher 빈 블록(detekt) 해결.before/after 콜백이 빈 구현이라 경고 발생합니다. no-op 주석을 추가하거나 doOnTextChanged로 대체하세요.
간단 적용 diff:
-override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} -override fun afterTextChanged(s: Editable?) {} +override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { /* no-op */ } +override fun afterTextChanged(s: Editable?) { /* no-op */ }(또는
androidx.core.widget.doOnTextChanged { text, _, _, _ -> ... }사용)
246-247: 문자열 하드코딩 제거.토스트 문구를 string 리소스로 분리하세요(현지화/일관성).
적용 diff:
-Toast.makeText(requireContext(), "최대 10개까지 선택할 수 있어요.", Toast.LENGTH_SHORT).show() +Toast.makeText(requireContext(), getString(R.string.search_filter_breed_limit_toast), Toast.LENGTH_SHORT).show()(리소스:
<string name="search_filter_breed_limit_toast">최대 10개까지 선택할 수 있어요.</string>)
89-96: 오탈자: setUpCalender → setUpCalendar.메서드/호출부 명칭을 올바르게 통일해주세요.
적용 diff:
- setUpCalender() + setUpCalendar()-private fun setUpCalender() = with(binding) { +private fun setUpCalendar() = with(binding) {Also applies to: 275-306
321-329: 구 목록 기본값 개선.없는 도시일 때
listOf("")대신"전체"로 처리하면 UX가 자연스럽습니다.적용 diff:
- val districtList = locationMap[newCity] ?: listOf("") + val districtList = locationMap[newCity] ?: listOf("전체")
118-129: 품종 드롭다운 필터 초기화 타이밍 보완.입력 불가/어댑터 미초기화 시 드롭다운을 닫는 로직은 좋습니다. 여기에 품종 입력이 공백으로 바뀐 경우
setBreedDropdown(false)를 추가하면 깔끔합니다.적용 diff:
- breedRvAdapter.filter.filter(token()) - setBreedDropdown(true) + val q = token() + breedRvAdapter.filter.filter(q) + setBreedDropdown(q.isNotEmpty())app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (4)
44-46: 미사용 필드 제거.
private lateinit var mapView는 사용되지 않습니다.적용 diff:
-private lateinit var mapView: MapView
92-100: 마커 이중 설정 정리.apply 블록 밖의 중복
position/map설정을 제거하세요.적용 diff:
- marker.position = location - marker.map = naverMap
63-66: cardId 파싱 안전화.비정상 값에 대비해 toLongOrNull로 파싱하세요.
적용 diff:
- cardId = args.id.ifBlank { cardId.toString() }.toLong() + cardId = args.id.toLongOrNull() ?: -1L
145-146: 태그 텍스트 설정 중복 제거/일관화.updateUI에서 직접 텍스트를 세팅하고, initTagView에서도 재세팅합니다. 하나로 통일하세요(아래처럼 initTagView만).
적용 diff:
- tvDetailTagField.text = convertTagToKorean(data.tag.text)추가로 initTagView는 그대로 유지(색/배경 포함)하면 충분합니다.
Also applies to: 213-220
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (4)
254-270: 그리드↔리스트 전환 시 데코레이션 정리 누락.리스트 모드로 돌아갈 때도 데코레이션을 제거해야 간격이 꼬이지 않습니다.
적용 diff:
} else { + while (binding.rvSearchHorizontalContent.itemDecorationCount > 0) { + binding.rvSearchHorizontalContent.removeItemDecorationAt(0) + } binding.rvSearchHorizontalContent.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) rvAdapter.setGridMode(false) binding.ibSearchHorizontalSort.setImageResource(R.drawable.ic_search_horizontal_sort) }
218-242: 무한 스크롤 중복 호출 방지.스크롤 업/정지 시에도 트리거될 수 있습니다. dy>0, 로딩 중 플래그 등으로 가드하세요.
적용 diff:
- if (rvPosition == totalCount) { + if (dy > 0 && rvPosition == totalCount) { viewModel.getSearchProtectData( lastProtectId, ) }
133-141: 중복 스크롤 호출 정리.
scrollToPosition(0)와smoothScrollToPosition(0)동시 호출은冗長합니다. 하나만 남기세요.적용 diff:
- binding.rvSearchHorizontalContent.scrollToPosition(0) - binding.rvSearchHorizontalContent.smoothScrollToPosition(0) + binding.rvSearchHorizontalContent.scrollToPosition(0)
259-263: 간격 상수 하드코딩 제거.픽셀 값 10 대신 dimen 리소스/DP 변환을 사용하세요.
적용 diff:
- binding.rvSearchHorizontalContent.addItemDecoration(SearchSpacingItemDecoration(10)) + val spacing = resources.getDimensionPixelOffset(R.dimen.search_grid_spacing) + binding.rvSearchHorizontalContent.addItemDecoration(SearchSpacingItemDecoration(spacing))(리소스:
<dimen name="search_grid_spacing">10dp</dimen>)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (5)
51-55: 더미 데이터는 디버그 빌드에서만 사용하도록 가드하세요.실제 배포 시 ViewModel 관찰이 비활성화되어 목록이 비어 보일 수 있어요. 빌드 타입으로 가드해 주세요.
-// observeViewModel() -// viewModel.getSearchReportData() - initDummyItems() - setupRV(items) + if (BuildConfig.DEBUG) { + initDummyItems() + setupRV(items) + } else { + observeViewModel() + viewModel.getSearchReportData() + }
60-119: 하드코딩된 더미 목록은 별도 픽스처로 분리하세요.프래그먼트가 장황해지고 리뷰/테스트가 어려워집니다. debugSource 혹은 FakeRepository로 이동을 권장합니다.
125-146: FragmentResult 전달 주체와 동일한 FragmentManager를 사용하세요.현재 childFragmentManager를 리스너에 사용 중입니다. 송신 측이 parentFragmentManager를 사용한다면 결과 수신이 되지 않습니다. 일치 여부 확인 후 필요 시 아래처럼 교체하세요.
- childFragmentManager.setFragmentResultListener( + parentFragmentManager.setFragmentResultListener( FILTER_RESULTS, viewLifecycleOwner ) { _, bundle ->
165-166: 중복 스크롤 호출 제거.scrollToPosition(0)와 smoothScrollToPosition(0)을 연속 호출할 필요가 없습니다. 하나만 남기세요.
- binding.rvSearchHorizontalContent.scrollToPosition(0) - binding.rvSearchHorizontalContent.smoothScrollToPosition(0) + binding.rvSearchHorizontalContent.scrollToPosition(0)
260-275: 그리드 ↔ 리스트 전환 시 ItemDecoration 정리 누락.리스트 모드로 돌아올 때도 데코를 제거해야 합니다(현재 그리드 진입 시에만 제거).
} else { - binding.rvSearchHorizontalContent.layoutManager = + while (binding.rvSearchHorizontalContent.itemDecorationCount > 0) { + binding.rvSearchHorizontalContent.removeItemDecorationAt(0) + } + binding.rvSearchHorizontalContent.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) rvAdapter.setGridMode(false) binding.ibSearchHorizontalSort.setImageResource(R.drawable.ic_search_horizontal_sort)app/src/main/res/layout/fragment_search_report.xml (1)
9-17: 배너 ImageView에도 contentDescription을 지정하세요.장식용이면 빈 문자열이 아닌 의미 있는 설명이나 android:importantForAccessibility="no"를 검토하세요.
<ImageView android:id="@+id/iv_search_banner" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="15dp" android:src="@drawable/img_search_banner_witness" + android:contentDescription="@string/cd_search_banner" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />strings.xml:
<string name="cd_search_banner">검색 배너</string>app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (4)
93-101: Marker 설정 중복 코드 제거.apply 블록 밖 동일 설정이 반복됩니다.
- val marker = Marker().apply { - position = location - map = naverMap - icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) - height = 23 - } - marker.position = location - marker.map = naverMap + val marker = Marker().apply { + position = location + map = naverMap + icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) + height = 23 + }
76-81: 북마크 리스너 이중 바인딩 제거.onViewCreated에서 initBookmarkUI() 호출 후 initListener() 내부에서 다시 호출합니다. 한 곳만 남기세요.
- initBookmarkUI() -// setContentVisibility() - initListener() + initListener()Also applies to: 162-167
135-152: UI 바인딩 필드 매핑 재확인 필요(중성화/발생일 혼선).tvValueNeuter ← happenDate 바인딩은 의미상 어긋나 보입니다. 도메인 필드와 뷰 id 대응을 다시 점검해 주세요.
원하시면 데이터-뷰 매핑 표 기반으로 일괄 리팩터링 패치 드릴게요.
43-47: 불필요한 필드 정리.lateinit var mapView는 사용되지 않습니다(naverMap은 필요). 제거로 가독성 ↑
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (2)
136-141: 중복 스크롤 호출 제거scrollToPosition(0)과 smoothScrollToPosition(0)을 동시에 호출할 필요는 없습니다. 하나만 유지하세요.
- binding.rvSearchHorizontalContent.scrollToPosition(0) - binding.rvSearchHorizontalContent.smoothScrollToPosition(0) + binding.rvSearchHorizontalContent.scrollToPosition(0)
264-274: 간격 값 하드코딩(픽셀) → 리소스 치수 사용SearchSpacingItemDecoration(10)에서 10은 픽셀로 해석됩니다. DP 기반 dimens로 교체해주세요.
- binding.rvSearchHorizontalContent.addItemDecoration(SearchSpacingItemDecoration(10)) + val spacing = resources.getDimensionPixelOffset(R.dimen.search_grid_spacing) + binding.rvSearchHorizontalContent.addItemDecoration(SearchSpacingItemDecoration(spacing))dimens에 search_grid_spacing가 없다면 추가 부탁드립니다.
app/src/main/res/layout/fragment_search_filter.xml (4)
252-260: 드롭다운 화살표 아이콘: 장식용은 접근성 제외화살표 아이콘들은 클릭 대상이 아니면 contentDescription을 null로 하고, 중요도 제외가 좋습니다.
- <ImageView + <ImageView android:id="@+id/iv_breed_arrow" @@ - android:layout_marginEnd="12dp"/> + android:layout_marginEnd="12dp" + android:contentDescription="@null" + android:importantForAccessibility="no" />동일 패턴을 iv_city_arrow, iv_district_arrow에도 적용해 주세요.
Also applies to: 366-374, 420-428
349-365: 도시/구 선택 영역 상호작용 명시AutoCompleteTextView가 focusable/cursor 비활성입니다. 사용자 제스처 진입점을 명확히 하세요(클릭 가능 처리 또는 부모 컨테이너에 클릭 리스너 부여).
- <AutoCompleteTextView + <AutoCompleteTextView android:id="@+id/actv_search_filter_city" @@ - android:cursorVisible="false" + android:cursorVisible="false" android:dropDownHeight="248dp" - android:focusable="false" + android:focusable="false" + android:clickable="true"구 필드에도 동일 적용 권장.
Also applies to: 403-419
262-278: Breed 드롭다운 컨테이너 고정 폭(320dp) → 제약 기반 가변 폭작은/큰 화면에서 레이아웃 왜곡이 날 수 있습니다. 부모 폭에 맞추는 제약으로 바꿔 주세요.
- <FrameLayout + <FrameLayout android:id="@+id/fl_filter_breed_container" - android:layout_width="320dp" + android:layout_width="0dp" android:layout_height="wrap_content" android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/actv_search_filter_breed" - app:layout_constraintStart_toStartOf="parent"> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent">
461-487: 버튼 텍스트 리소스화 및 일관성Reset 버튼 "초기화" 하드코딩을 strings로 이동해 주세요. Confirm은 이미 리소스 사용 중입니다.
- <androidx.appcompat.widget.AppCompatButton + <androidx.appcompat.widget.AppCompatButton android:id="@+id/btn_search_filter_reset" @@ - android:text="초기화" + android:text="@string/search_filter_reset"app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (3)
51-56: inflate 인자 누락: container/attachToParent 반영Fragment 바인딩 inflate는 inflater, container, attachToParent=false를 쓰는 게 안전합니다. 현재는 레이아웃 파라미터가 누락될 수 있어요.
- binding = FragmentSearchDetailDisappearBinding.inflate(layoutInflater) + binding = FragmentSearchDetailDisappearBinding.inflate(inflater, container, false)
196-206: 더미 이미지 초기화는 개발 모드 한정상세 진입 시 항상 더미 이미지를 세팅하고 있어 실데이터를 덮습니다.
- private fun initListener() = with(binding) { - initDummyImages() + private fun initListener() = with(binding) { + if (BuildConfig.DEBUG) initDummyImages()실데이터 바인딩 시점(updateUI)에서 이미지가 없을 때만 더미로 보완하는 것도 방법입니다.
242-267: 중복 유틸 메서드 정리 제안: openNaverMap동일 구현이 다른 상세 프래그먼트에도 존재합니다. 공통 유틸/베이스로 추출하면 유지보수가 쉬워집니다.
원하시면 SearchUiUtils.kt(또는 BaseDetailFragment)로 추출 패치 드릴게요.
app/src/main/res/layout/fragment_search_detail_protecting.xml (6)
39-45: ScrollView 높이 0dp 사용 시 fillViewport 추가 권장0dp+제약 패턴은 좋지만, 콘텐츠가 적을 때 하단 버튼이 뜨는 이슈 방지를 위해 fillViewport 추가를 권장합니다.
<ScrollView android:id="@+id/sv_search_detail" android:layout_width="match_parent" - android:layout_height="0dp" + android:layout_height="0dp" + android:fillViewport="true" app:layout_constraintBottom_toBottomOf="parent"
51-60: nestedScrollingEnabled 불필요단일 ImageView에 nestedScrollingEnabled="false"는 의미 없습니다. 제거 권장합니다.
- android:nestedScrollingEnabled="false"
305-363: 주소/복사 라벨 하드코딩 및 색상 리터럴
- 주소, "복사" 라벨이 하드코딩입니다 → strings.xml로 이동, 런타임 텍스트는 바인딩으로 주입하고 프리뷰는 tools:text 사용.
- "#00D1CA" 하드코딩 색상은 @color 참조로 통일하세요.
- android:text="충청남도 예산군 오가면 윤봉길로 1983-10, 한진고물상" + tools:text="@string/_sample_address" ... - android:text="복사" + android:text="@string/common_copy" - android:textColor="#00D1CA" + android:textColor="@color/main_color" + android:contentDescription="@string/cd_copy_address"
276-295: MapView를 ScrollView 안에 둘 때 스크롤 충돌 대응MapView는 NestedScrolling을 지원하지 않아 제스처 충돌이 생길 수 있습니다. NestedScrollView로 교체하거나 터치 시 부모 인터셉트를 막아 주세요.
// Fragment 예시 binding.mapView.setOnTouchListener { v, _ -> v.parent.requestDisallowInterceptTouchEvent(true) false }원하면 NestedScrollView로의 마이그레이션 패치도 제공할게요.
280-285: circularflow_defaultRadius는 무의미한 속성CircularFlow 헬퍼 전용 속성입니다. 현재 레이아웃에선 효과가 없으니 제거 권장합니다.
- app:circularflow_defaultRadius="20dp"
553-612: 하단 버튼 영역 클릭 반응/접근성 개선
- 두 LinearLayout을 실제 클릭 타겟으로 사용한다면 selectableItemBackground 적용, contentDescription 부여를 권장합니다.
- 아이콘은 decorative면 contentDescription="@null"과 importantForAccessibility로 라벨 중복을 피하세요.
- <LinearLayout + <LinearLayout android:id="@+id/ll_call_phone" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" - android:orientation="horizontal"> + android:orientation="horizontal" + android:background="?attr/selectableItemBackground" + android:contentDescription="@string/search_action_call"> ... - <ImageView + <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginEnd="6dp" android:src="@drawable/ic_search_phone" app:tint="@color/main_color" />app/src/main/res/layout/fragment_search_detail_disappear.xml (5)
41-47: fillViewport 추가 제안ScrollView 0dp 패턴과 함께 fillViewport 사용을 권장합니다.
<ScrollView android:id="@+id/sv_search_detail" android:layout_width="match_parent" - android:layout_height="0dp" + android:layout_height="0dp" + android:fillViewport="true"
256-267: MapView 스크롤 충돌 가능성ScrollView 내 MapView는 제스처 충돌 가능성이 큽니다. NestedScrollView 전환 또는 터치 인터셉트 방지 코드를 Fragment에 추가해 주세요.
동일 패턴은 Protecting/Witness 화면에도 적용 요망.
Also applies to: 268-274
260-265: circularflow_defaultRadius 제거 권장CircularFlow 미사용 상태에서 의미 없는 속성입니다.
- app:circularflow_defaultRadius="20dp"
286-305: 위치/복사 블록: 하드코딩/색상 리터럴/접근성
- 위치 주변 텍스트, 주소, "복사" 라벨 하드코딩 → strings.xml로 이동, 런타임 값은 바인딩.
- "#00D1CA" → @color 리소스 사용.
- 복사 아이콘에 contentDescription 부여.
- android:text="주변에서 실종됐어요." + android:text="@string/search_disappear_around_suffix" ... - android:text="충청남도 예산군 ..." + tools:text="@string/_sample_address" ... - android:text="복사" + android:text="@string/common_copy" - android:textColor="#00D1CA" + android:textColor="@color/main_color" + android:contentDescription="@string/cd_copy_address"Also applies to: 306-316, 317-345
543-604: 하단 버튼 네거티브 마진 제거 및 클릭 피드백 추가
- 아이콘에 layout_marginStart="-10dp"는 레이아웃 예측을 어렵게 합니다. 패딩/정렬로 조정하세요.
- 버튼 컨테이너에 selectableItemBackground와 contentDescription 추가.
- android:layout_marginStart="-10dp" + android:layout_marginStart="0dp" ... - <LinearLayout + <LinearLayout android:id="@+id/ll_view_map" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" - android:orientation="horizontal"> + android:orientation="horizontal" + android:background="?attr/selectableItemBackground" + android:contentDescription="@string/search_action_open_map">app/src/main/res/layout/fragment_search_detail_witness.xml (4)
176-185: 지도 섹션 스크롤/제스처 가이드MapView가 ScrollView 안에 있어 제스처 충돌 소지가 있습니다. NestedScrollView 전환 또는 부모 인터셉트 방지 코드를 추가해 주세요. Protecting/Disappear와 동일 권고.
Also applies to: 186-197
236-274: 위치/복사 블록 하드코딩/색상 리터럴/접근성주소/“복사”/색상 하드코딩을 리소스로 이전하고 아이콘에 contentDescription을 추가해 주세요.
- android:text="충청남도 예산군 오가면 윤봉길로 1983-10, 한진고물상" + tools:text="@string/_sample_address" ... - android:text="복사" + android:text="@string/common_copy" - android:textColor="#00D1CA" + android:textColor="@color/main_color" + android:contentDescription="@string/cd_copy_address"
417-431: 하단 버튼: 네거티브 마진 제거 및 클릭 피드백/접근성
- layout_marginStart="-10dp" 제거하고 패딩/정렬로 보정하세요.
- 컨테이너에 selectableItemBackground와 contentDescription 추가.
- android:layout_marginStart="-10dp" + android:layout_marginStart="0dp" ... - <LinearLayout + <LinearLayout android:id="@+id/ll_view_map" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" - android:orientation="horizontal"> + android:orientation="horizontal" + android:background="?attr/selectableItemBackground" + android:contentDescription="@string/search_action_open_map">
190-195: circularflow_defaultRadius 제거 권장CircularFlow 미사용이므로 속성은 무의미합니다.
- app:circularflow_defaultRadius="20dp"
...c/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
Show resolved
Hide resolved
...c/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
Show resolved
Hide resolved
.../main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt
Show resolved
Hide resolved
.../main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt
Show resolved
Hide resolved
...src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt
Show resolved
Hide resolved
ikseong00
left a comment
There was a problem hiding this comment.
오랜만에 View 코드 보니 익숙치 않네요
혹시 실행 영상도 올려주실 수 있나요 ??
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchBreedRVAdapter.kt
Show resolved
Hide resolved
스크롤 범위만 수정하고 바로 올리겠습니다!! |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (1)
69-71: ID 파싱 안전화 및 NavGraph argType 변경 고려
문자열 ID가 숫자 형식이 아닐 경우 NumberFormatException이 발생할 수 있습니다. 아래처럼 안전 파싱을 적용하거나nav_graph.xml에서<argument android:name="id" app:argType="long" />으로 변경하세요.- cardId = args.id.ifBlank { cardId.toString() }.toLong() + cardId = args.id.toLongOrNull() ?: -1L - tag = args.tag.ifBlank { tag } + tag = args.tag.ifBlank { null } - name = args.name.ifBlank { name } + name = args.name.ifBlank { null }NavGraph에서
id를long타입으로 변경하면cardId = args.id처럼 더 단순하게 사용할 수 있습니다.app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (1)
119-133: Flow 수집을 View 생명주기에 맞춰 안전화
lifecycleScope.launch { collectLatest { ... } }는 뷰 파괴 후에도 수집을 이어갈 수 있어요.viewLifecycleOwner+repeatOnLifecycle(STARTED)로 감싸 주세요.참고 구현:
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.detailSearchData.collectLatest { it?.let(::updateUI) } } launch { viewModel.errorMessage.collectLatest { it?.let { Log.e("DetailSearchViewModel", it); Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() } } } } }
♻️ Duplicate comments (11)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (3)
52-56: setupMap 호출 시점 이동 (주소 세팅 후).지도 초기화가 onCreateView 단계에서 너무 이르게 실행돼 주소가 비어 있을 수 있어요. UI 바인딩 완료 후 호출로 이동해 주세요.
@@ - binding.mapView.onCreate(savedInstanceState) - binding.mapView.getMapAsync { nMap -> - naverMap = nMap - setupMap() - } + binding.mapView.onCreate(savedInstanceState) + binding.mapView.getMapAsync { nMap -> + naverMap = nMap + } @@ - tvWitnessDate.text = data.eventDate + tvWitnessDate.text = data.eventDate + // 주소/필드 세팅 이후 지도 반영 + setupMap() -// initViewPager(data.imageUrls) initTagView(data) -// initBookmarkUI(data) -// initMapButtons(data)Also applies to: 151-156
81-105: Geocoder 메인 스레드 호출/예외 처리 개선 + 오타 수정.지오코딩은 IO 스레드로, UI 업데이트만 Main에서. 토스트 리소스화와 로그 남김으로 detekt 경고도 해소하세요.
필요 import:
- android.location.Geocoder
- kotlinx.coroutines.Dispatchers
- kotlinx.coroutines.withContext
@@ - private fun setupMap() { - val address = binding.tvValueWitnessLocation.text.toString() - if (address.isNotEmpty()) { - try { - val geocoder = android.location.Geocoder(requireContext()) - val results = geocoder.getFromLocationName(address, 1) - if (!results.isNullOrEmpty()) { - val location = LatLng(results[0].latitude, results[0].longitude) - val cameraUpdate = CameraUpdate.scrollTo(location) - naverMap?.moveCamera(cameraUpdate) - - val marker = Marker().apply { - position = location - map = naverMap - icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) - height = 23 - } - marker.position = location - marker.map = naverMap - } - } catch (e: Exception) { - Toast.makeText(requireContext(), "주소 찾을 수 업음.", Toast.LENGTH_SHORT).show() - } - } - } + private fun setupMap() { + val address = binding.tvValueWitnessLocation.text.toString() + if (address.isBlank()) return + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + runCatching { + val geocoder = Geocoder(requireContext()) + geocoder.getFromLocationName(address, 1) + }.onSuccess { results -> + if (!results.isNullOrEmpty()) { + val location = LatLng(results[0].latitude, results[0].longitude) + val cameraUpdate = CameraUpdate.scrollTo(location) + withContext(Dispatchers.Main) { + naverMap?.moveCamera(cameraUpdate) + Marker().apply { + position = location + map = naverMap + icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) + height = 23 + } + } + } + }.onFailure { e -> + withContext(Dispatchers.Main) { + Toast.makeText( + requireContext(), + getString(R.string.search_error_geocode_fail), + Toast.LENGTH_SHORT + ).show() + } + Log.e("SearchWitnessDetail", "Geocoding failed: $address", e) + } + } + }리소스가 없다면 추가해 주세요:
주소를 찾을 수 없어요.
51-57: MapView 라이프사이클 위임 추가.onStart/onResume/onPause/onStop/onDestroyView/onLowMemory/onSaveInstanceState 위임이 없어 누수·흰 화면 위험.
override fun onStart() { super.onStart() binding.mapView.onStart() } override fun onResume() { super.onResume() binding.mapView.onResume() } override fun onPause() { binding.mapView.onPause() super.onPause() } override fun onStop() { binding.mapView.onStop() super.onStop() } override fun onDestroyView() { binding.mapView.onDestroy() super.onDestroyView() } override fun onLowMemory() { super.onLowMemory() binding.mapView.onLowMemory() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) binding.mapView.onSaveInstanceState(outState) }app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (2)
57-61: MapView 생명주기 위임 누락 — 릭/검은 화면 이슈 가능onCreate만 위임되어 있어요. Fragment 생명주기 전체로 위임 추가해 주세요.
적용 예시:
@@ binding = FragmentSearchDetailDisappearBinding.inflate(layoutInflater) @@ - binding.mapView.onCreate(savedInstanceState) + binding.mapView.onCreate(savedInstanceState) binding.mapView.getMapAsync { nMap -> naverMap = nMap setupMap() } return binding.root } + override fun onStart() { + super.onStart() + binding.mapView.onStart() + } + + override fun onResume() { + super.onResume() + binding.mapView.onResume() + } + + override fun onPause() { + binding.mapView.onPause() + super.onPause() + } + + override fun onStop() { + binding.mapView.onStop() + super.onStop() + } + + override fun onLowMemory() { + super.onLowMemory() + binding.mapView.onLowMemory() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.mapView.onSaveInstanceState(outState) + } + + override fun onDestroyView() { + binding.mapView.onDestroy() + super.onDestroyView() + }
84-108: Geocoder를 메인 스레드에서 동기 호출 + 예외 삼킴/오탈자UI 스레드 블로킹과 detekt 경고(예외 삼킴)가 있어요. IO 디스패처로 오프로드하고, 문자열은 리소스로 분리하며 로그 남겨주세요.
- private fun setupMap() { - val address = binding.tvValueLostLocation.text.toString() - if (address.isNotEmpty()) { - try { - val geocoder = android.location.Geocoder(requireContext()) - val results = geocoder.getFromLocationName(address, 1) - if (!results.isNullOrEmpty()) { - val location = LatLng(results[0].latitude, results[0].longitude) - val cameraUpdate = CameraUpdate.scrollTo(location) - naverMap?.moveCamera(cameraUpdate) - - val marker = Marker().apply { - position = location - map = naverMap - icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) - height = 23 - } - marker.position = location - marker.map = naverMap - } - } catch (e: Exception) { - Toast.makeText(requireContext(), "주소 찾을 수 업음.", Toast.LENGTH_SHORT).show() - } - } - } + private fun setupMap() { + val address = binding.tvValueLostLocation.text.toString() + if (address.isBlank()) return + lifecycleScope.launch(Dispatchers.IO) { + runCatching { + val geocoder = android.location.Geocoder(requireContext()) + geocoder.getFromLocationName(address, 1) + }.onSuccess { results -> + if (!results.isNullOrEmpty()) { + val location = LatLng(results[0].latitude, results[0].longitude) + withContext(Dispatchers.Main) { + naverMap?.moveCamera(CameraUpdate.scrollTo(location)) + Marker().apply { + position = location + map = naverMap + icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) + height = 23 + } + } + } else { + withContext(Dispatchers.Main) { + Toast.makeText(requireContext(), getString(R.string.search_address_not_found), Toast.LENGTH_SHORT).show() + } + } + }.onFailure { e -> + Log.w("SearchDisappearDetail", "Geocoding failed", e) + withContext(Dispatchers.Main) { + Toast.makeText(requireContext(), getString(R.string.search_address_not_found), Toast.LENGTH_SHORT).show() + } + } + } + }추가 필요(import):
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContextstrings.xml에 R.string.search_address_not_found 추가(“주소를 찾을 수 없음.” 등).
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (1)
48-54: 더미 데이터가 프로덕션 코드에 하드코딩되어 있습니다실제 ViewModel 데이터 관찰이 주석 처리되고 더미 데이터가 직접 사용되고 있어, 프로덕션에서 문제가 될 수 있습니다.
BuildConfig.DEBUG 가드를 사용하여 개발 환경에서만 더미 데이터를 사용하도록 수정하세요:
initRVAdapter() -// observeViewModel() -// viewModel.getSearchReportData() - initDummyItems() - setupRV(items) + if (BuildConfig.DEBUG) { + initDummyItems() + setupRV(items) + } else { + observeViewModel() + viewModel.getSearchReportData() + }app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (2)
54-113: 프로덕션 코드에 더미 데이터 하드코딩SearchReportFragment와 동일한 문제로, 더미 데이터가 프로덕션 코드에 직접 포함되어 있습니다.
48-50: 실데이터 관찰이 비활성화되어 더미 데이터만 사용 중입니다observeViewModel()과 getSearchAllData() 호출이 주석 처리된 상태에서 더미 데이터만 사용하고 있어, 스크롤 페이징이나 필터 기능이 제대로 동작하지 않을 수 있습니다.
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (2)
53-57: MapView 라이프사이클 모두 위임 필요 (크래시/누수 위험)onCreate만 호출되고 있어요. Fragment 수명주기에 맞춰 mapView 콜백을 모두 위임해 주세요. 기존 코멘트와 동일 이슈입니다.
추가 코드:
override fun onStart() { super.onStart(); binding.mapView.onStart() } override fun onResume() { super.onResume(); binding.mapView.onResume() } override fun onPause() { binding.mapView.onPause(); super.onPause() } override fun onStop() { binding.mapView.onStop(); super.onStop() } override fun onLowMemory() { super.onLowMemory(); binding.mapView.onLowMemory() } override fun onDestroyView() { binding.mapView.onDestroy(); super.onDestroyView() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState); binding.mapView.onSaveInstanceState(outState) }
82-105: 지오코딩 메인 스레드 호출 + 예외 삼킴 + 토스트 오탈자Geocoder는 블로킹입니다. IO 디스패처로 이동하고, 로그 남기고, 중복 마커 할당 제거, 오탈자(“업음”→“없음”)를 함께 정리해 주세요.
- private fun setupMap() { - val address = binding.tvValueProtectLocation.text.toString() - if (address.isNotEmpty()) { - try { - val geocoder = android.location.Geocoder(requireContext()) - val results = geocoder.getFromLocationName(address, 1) - if (!results.isNullOrEmpty()) { - val location = LatLng(results[0].latitude, results[0].longitude) - val cameraUpdate = CameraUpdate.scrollTo(location) - naverMap?.moveCamera(cameraUpdate) - - val marker = Marker().apply { - position = location - map = naverMap - icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) - height = 23 - } - marker.position = location - marker.map = naverMap - } - } catch (e: Exception) { - Toast.makeText(requireContext(), "주소 찾을 수 업음.", Toast.LENGTH_SHORT).show() - } - } - } + private fun setupMap() { + val address = binding.tvValueProtectLocation.text.toString() + if (address.isBlank()) return + viewLifecycleOwner.lifecycleScope.launch { + try { + val geocoder = android.location.Geocoder(requireContext()) + val results = withContext(kotlinx.coroutines.Dispatchers.IO) { + geocoder.getFromLocationName(address, 1) + } + if (!results.isNullOrEmpty()) { + val location = LatLng(results[0].latitude, results[0].longitude) + naverMap?.moveCamera(CameraUpdate.scrollTo(location)) + Marker().apply { + position = location + icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) + height = 23 + map = naverMap + } + } else { + Toast.makeText(requireContext(), "주소를 찾을 수 없음.", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + Log.e("SearchProtectingDetail", "지오코딩 실패", e) + Toast.makeText(requireContext(), "주소를 찾을 수 없음.", Toast.LENGTH_SHORT).show() + } + } + }app/src/main/res/layout/item_search_header.xml (1)
21-39: 아이콘 버튼 접근성/터치 피드백/터치 타깃 보강 필요.
- ripple 미적용(background="@null"), CD 없음, 36dp로 최소 터치 타깃(48dp) 미만.
- 기존 파일에서도 같은 지적이 있었어요. 일관 반영 부탁드립니다.
<ImageButton android:id="@+id/ib_search_filter" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@null" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="48dp" + android:minHeight="48dp" + android:padding="12dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/cd_search_filter" app:layout_constraintBottom_toBottomOf="parent" android:src="@drawable/img_search_filters" /> <ImageButton android:id="@+id/ib_search_horizontal_sort" - android:layout_width="36dp" + android:layout_width="36dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - android:layout_height="36dp" - android:background="@null" + android:layout_height="36dp" + android:minWidth="48dp" + android:minHeight="48dp" + android:padding="6dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/cd_search_toggle_layout" android:src="@drawable/ic_search_horizontal_sort" />strings.xml에도 다음을 추가해 주세요:
<string name="cd_search_filter">필터 열기</string> <string name="cd_search_toggle_layout">레이아웃 전환</string>
🧹 Nitpick comments (31)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (4)
44-45: 불필요한 필드 제거.lateinit mapView는 사용되지 않아요. binding.mapView만 유지하세요.
- private lateinit var mapView: MapView private var naverMap: NaverMap? = null
126-140: Flow 수집을 뷰 수명주기에 맞추기.Fragment.lifecycleScope 대신 viewLifecycleOwner + repeatOnLifecycle 사용 권장.
필요 import: androidx.lifecycle.repeatOnLifecycle, androidx.lifecycle.Lifecycle
- private fun observeViewModel() { - lifecycleScope.launch { - viewModel.detailSearchData.collectLatest { data -> - data?.let { updateUI(it) } - } - } - - lifecycleScope.launch { - viewModel.errorMessage.collectLatest { message -> - message?.let { - Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() - } - } - } - } + private fun observeViewModel() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.lifecycle.repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.STARTED) { + launch { + viewModel.detailSearchData.collectLatest { data -> + data?.let { updateUI(it) } + } + } + launch { + viewModel.errorMessage.collectLatest { message -> + message?.let { + Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() + } + } + } + } + } + }
145-156: 태그 텍스트 중복 세팅 제거.updateUI에서 직접 텍스트 세팅 후 initTagView에서 다시 세팅합니다. 한 곳으로 통일하세요.
- tvDetailTagField.text = convertTagToKorean(data.tag.text) @@ initTagView(data)
205-211: 북마크 토글 실패 시 UI 롤백 고려.API 실패 시 아이콘이 실제 상태와 불일치할 수 있어요. 성공 콜백에서만 updateBookmarkUI 하거나 실패 시 isBookmarked를 원복하세요.
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (7)
95-103: Marker 중복 할당 제거apply 블록 밖에서 position/map을 다시 설정하고 있어요. 불필요합니다.
val marker = Marker().apply { position = location map = naverMap icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) height = 23 } - marker.position = location - marker.map = naverMap
48-49: 불필요한 필드 제거lateinit var mapView는 사용되지 않습니다(binding.mapView만 사용). 제거해 깔끔히 유지하세요.
- private lateinit var mapView: MapView private var naverMap: NaverMap? = null
74-74: 하드코딩 문자열 → strings.xml로 이동 + 오탈자 수정UI 문자열은 리소스로 관리하고 다국어 대응하세요. “업음” → “없음”.
- Toast.makeText(requireContext(), "잘못된 접근입니다.", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), getString(R.string.error_invalid_access), Toast.LENGTH_SHORT).show() @@ - Toast.makeText(requireContext(), "잘못된 태그 값입니다.", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), getString(R.string.error_invalid_tag_value), Toast.LENGTH_SHORT).show() @@ - Toast.makeText(requireContext(), "준비 중이에요!", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), getString(R.string.common_preparing_message), Toast.LENGTH_SHORT).show()strings.xml에 다음 키 추가:
- error_invalid_access
- error_invalid_tag_value
- common_preparing_message
- search_address_not_found
Also applies to: 123-123, 205-205
110-117: 더미 이미지 로딩은 디버그 빌드에서만실 배포에서 더미 리소스가 노출되지 않도록 BuildConfig.DEBUG 가드 권장.
- private fun initDummyImages() { + private fun initDummyImages() { val dummyImages = listOf( R.drawable.img_search_detail_content, R.drawable.img_search_detail_content, R.drawable.img_search_detail_content ) initViewPager(dummyImages) } @@ - initDummyImages() + if (com.example.findu.BuildConfig.DEBUG) { + initDummyImages() + }Also applies to: 197-198
149-158: null 표시 방지 가드서버 값 누락 시 “null” 텍스트 노출 가능. orEmpty()/?: ""로 가드 권장.
- tvValueAge.text = data.age - tvValueGender.text = data.sex - tvValueLostDate.text = data.eventDate - tvValueRfid.text = data.rfid - tvSpecialNote.text = data.specialNote - tvValueLostLocationAround.text = data.surroundPlace - tvValueLostLocation.text = data.eventLocation - tvValueReporterName.text = data.userName - tvValuePhoneNumber.text = data.userPhone + tvValueAge.text = data.age.orEmpty() + tvValueGender.text = data.sex.orEmpty() + tvValueLostDate.text = data.eventDate.orEmpty() + tvValueRfid.text = data.rfid.orEmpty() + tvSpecialNote.text = data.specialNote.orEmpty() + tvValueLostLocationAround.text = data.surroundPlace.orEmpty() + tvValueLostLocation.text = data.eventLocation.orEmpty() + tvValueReporterName.text = data.userName.orEmpty() + tvValuePhoneNumber.text = data.userPhone.orEmpty()
242-267: openNaverMap 중복 로직 유틸로 추출하기
SearchWitnessDetailFragment, SearchDisappearDetailFragment, SearchProtectingDetailFragment 에서 거의 동일한openNaverMap구현이 반복됩니다. NaverMap 전용 유틸 객체나 확장 함수로 분리해 재사용성을 높이세요.예시:
object NaverMapUtil { fun openSearch(context: Context, address: String) { if (address.isEmpty()) return val encoded = Uri.encode(address) val nmapUri = Uri.parse("nmap://search?query=$encoded&appname=${context.packageName}") val intent = Intent(Intent.ACTION_VIEW, nmapUri) when { intent.resolveActivity(context.packageManager) != null -> context.startActivity(intent) else -> { val market = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.nhn.android.nmap")) try { context.startActivity(market) } catch (e: ActivityNotFoundException) { context.startActivity(Intent( Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.nhn.android.nmap") )) } } } } }프래그먼트에서는 아래처럼 호출:
NaverMapUtil.openSearch(requireContext(), address)
79-80: observeViewModel()/fetchDetailData() 복구 필요
주석 처리로 데이터 로딩이 완전히 비활성화되어 있습니다. 의도적이라면// TODO: …주석을 추가해두고, 아니라면 아래처럼 복구하세요.-// observeViewModel() -// fetchDetailData() + observeViewModel() + fetchDetailData()app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (1)
59-118: 더미 데이터를 별도 클래스로 분리 고려60줄에 달하는 더미 데이터가 Fragment 내부에 하드코딩되어 있어 코드 가독성을 해칩니다.
DummyDataProvider 클래스로 분리하거나 debug 소스셋으로 이동하는 것을 고려해보세요:
object SearchDummyDataProvider { fun getReportItems(): List<SearchAnimal> { return arrayListOf( // 더미 데이터... ) } }app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchListAdapter.kt (8)
27-46: DiffUtil는 OK, 하지만 Header 동등성 정의는 장기적으로 ID 부여가 더 안전합니다Header↔Header를 항상 동일 취급(true)하는 전략은 애니메이션/이동 시 예외가 생길 수 있어요. 안정적 아이디를 쓰면 스크롤/애니메이션 품질이 좋아집니다.
적용 예:
class SearchListAdapter( @@ -) : ListAdapter<SearchListItem, RecyclerView.ViewHolder>(DIFF) { +) : ListAdapter<SearchListItem, RecyclerView.ViewHolder>(DIFF) { + init { + setHasStableIds(true) + } @@ companion object { @@ private val DIFF = object : DiffUtil.ItemCallback<SearchListItem>() { @@ } } + + override fun getItemId(position: Int): Long = when (val item = getItem(position)) { + is SearchListItem.Header -> Long.MIN_VALUE + is SearchListItem.Content -> item.data.cardId + }
48-53: 그리드 전환에 notifyDataSetChanged 전면 갱신은 과합니다뷰타입만 바뀌니 콘텐츠(1..n) 범위만 갱신하거나 payload를 써서 최소 갱신이 좋아요.
간단 대안:
- notifyDataSetChanged() + // Header(0)는 동일, 콘텐츠만 리바인드 + if (itemCount > 1) notifyItemRangeChanged(1, itemCount - 1)
81-87: onBindViewHolder에서 안전 캐스팅과 payload 처리를 고려해주세요다운캐스트가 많아 가독성이 떨어집니다. 또한 북마크만 변경될 때 payload로 아이콘만 바꾸면 깜빡임이 줄어듭니다.
짧은 예시(개념):
- is GridVH -> holder.bind((getItem(position) as SearchListItem.Content).data) - is HorizontalVH -> holder.bind((getItem(position) as SearchListItem.Content).data) + is GridVH -> (getItem(position) as? SearchListItem.Content)?.let { holder.bind(it.data) } + is HorizontalVH -> (getItem(position) as? SearchListItem.Content)?.let { holder.bind(it.data) }payload는
onBindViewHolder(holder, pos, payloads)오버로드 추가로 처리.
97-118: 어댑터 내부에서 모델을 직접 mutate(토글)하면 일관성 깨질 수 있어요
item.isBookmark = !item.isBookmark는 VM/서버 반영 전 임시 상태를 UI에 고정시켜 재바인딩 시 되돌아갈 수 있습니다. 이벤트만 올리고, 결과 리스트로 갱신 받는 패턴을 권장합니다.간단 대안:
- ivSearchContentBookmark.setOnClickListener { - item.isBookmark = !item.isBookmark - onBookmarkClick(item.cardId, item.isBookmark, item.tag.text) - updateBookmarkIcon(item.isBookmark) - } + ivSearchContentBookmark.setOnClickListener { + onBookmarkClick(item.cardId, !item.isBookmark, item.tag.text) + }그리고 VM에서 갱신된 리스트를 submitList로 반영해 주세요.
100-111: 이미지 로딩 placeholder/error 및 크기 옵션 추가 권장네트워크 지연/실패 시 공백 방지용 플레이스홀더와 에러 이미지를 지정하면 UX가 좋아집니다.
- Glide.with(root.context).load(item.image).into(ivSearchContent) + Glide.with(root.context) + .load(item.image) + .placeholder(R.drawable.ic_image_placeholder) + .error(R.drawable.ic_image_placeholder) + .centerCrop() + .into(ivSearchContent)
121-126: 중복된 북마크 아이콘 갱신 로직을 하나로 합치기Grid/Horizontal에 동일한 메서드가 중복됩니다. 유틸로 분리해 중복 제거합시다.
+private fun ImageView.setBookmarkIcon(isBookmarked: Boolean) { + setImageResource(if (isBookmarked) R.drawable.ic_search_fill_bookmark else R.drawable.ic_search_blank_bookmark) +}그리고 각 ViewHolder에서
binding.ivSearchContentBookmark.setBookmarkIcon(isBookmarked)호출로 대체.Also applies to: 153-158
100-118: 접근성: contentDescription 설정 누락이미지와 북마크 버튼에 contentDescription이 없어 스크린리더 사용자에게 불친절합니다.
예:
tvSearchContentName.text = item.name +ivSearchContent.contentDescription = item.name +ivSearchContentBookmark.contentDescription = + root.context.getString(if (item.isBookmark) R.string.cd_bookmarked else R.string.cd_bookmark)Also applies to: 132-150
89-95: 토글 버튼 아이콘 상태 동기화 필요Header에서 토글 클릭만 전달하고 실제 아이콘 상태 전환은 안 합니다. 현재
setGridMode후notify…로 전체 리바인드하더라도 Header가 상태를 표시하려면 isGridMode를 바인딩에 반영하거나notifyItemChanged(0)로 헤더만 갱신하는 처리가 필요합니다.app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (2)
238-256: 그리드→리스트 전환 시 ItemDecoration이 남습니다Decoration 제거를 리스트 전환 블록에서도 수행해야 리스트 모드 간격이 정상화됩니다.
- } else { - binding.rvSearchRescue.layoutManager = - LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) - listAdapter.setGridMode(false) - } + } else { + while (binding.rvSearchRescue.itemDecorationCount > 0) { + binding.rvSearchRescue.removeItemDecorationAt(0) + } + binding.rvSearchRescue.layoutManager = + LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) + listAdapter.setGridMode(false) + }
142-169: 하드코딩된 태그 문자열 대신 공통 enum/리소스를 사용하세요"보호중/목격신고/실종신고" 문자열 비교는 오타/로케일 의존성이 큽니다.
SearchStatus를 기준으로 분기하거나,toSearchRvTag()가 제공하는 타입/코드로 분기하는 게 안전합니다.app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (5)
45-47: 불필요한 필드 정리: mapView 미사용
binding.mapView만 사용 중이라private lateinit var mapView: MapView는 제거해도 됩니다.- private lateinit var mapView: MapView
162-177: initBookmarkUI 중복 호출 제거
onViewCreated(Line 76)에서 이미 호출합니다. 여기서 다시 호출하면 리스너 재설정으로 혼동 여지가 있어요. 한 곳만 남겨 주세요.- initBookmarkUI()
189-197: 주석 처리된 죽은 코드 정리사용하지 않는 블록은 삭제해 주세요. 유지 보수성만 떨어집니다.
-// private fun initMapButtons(data: DetailProtectData) { -// binding.btnViewLocation.setOnClickListener { -// openNaverMap(data.careAddr) -// } -// binding.btnShowFoundPlace.setOnClickListener { -// openNaverMap(data.foundLocation) -// } -// }
198-203: 북마크 토글 시 API 호출 의도 확인토글 상태와 무관하게 매번
setInterestProtectingAnimal(cardId)만 호출합니다. 해제(언북마크) 시 동작 정의가 따로 있다면 분기 필요합니다.예:
binding.ivSearchDetailBookmark.setOnClickListener { val next = !isBookmarked if (next) viewModel.setInterestProtectingAnimal(cardId) else viewModel.unsetInterestProtectingAnimal(cardId) // 존재 시 isBookmarked = next updateBookmarkUI(isBookmarked) }
252-256: 북마크 아이콘 리소스 통일성 확인다른 상세 프래그먼트(실종/목격)와 else 아이콘 리소스가 동일한지 확인해 주세요. UI 일관성을 위해 하나로 맞추는 걸 권장합니다.
app/src/main/java/com/example/findu/presentation/ui/search/SearchSpacingItemDecoration.kt (1)
27-39: 그리드 수직 간격이 없어 행 간격이 붙어 보일 수 있어요. 상·하 간격도 부여하세요.
현재 GridLayout에서는 좌/우만 주고 있어 첫 행/다음 행 간격이 없음. 아래처럼 상·하 절반 간격을 더해 균일한 거터를 만드는 것을 권장합니다.if (layoutManager is GridLayoutManager) { - val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams - val index = layoutParams.spanIndex - if (index == 0) { - outRect.right = spacing / 2 - } else { - outRect.left = spacing / 2 - } + val lp = view.layoutParams as GridLayoutManager.LayoutParams + val index = lp.spanIndex + // 세로 간격 균일화 + outRect.top = spacing / 2 + outRect.bottom = spacing / 2 + // 좌우 거터 균일화 + if (index == 0) outRect.right = spacing / 2 else outRect.left = spacing / 2 } else { outRect.top = spacing / 2 outRect.bottom = spacing / 2 }app/src/main/res/layout/item_search_header.xml (1)
7-14: 배너가 장식용이라면 접근성에서 제외하세요.
스크린리더가 쓸모없는 배너 이미지를 읽지 않도록 설정 권장.<ImageView android:id="@+id/iv_search_banner" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" android:layout_width="match_parent" android:layout_height="wrap_content" - android:src="@drawable/img_search_banner_adopt" /> + android:src="@drawable/img_search_banner_adopt" + android:contentDescription="@null" + android:importantForAccessibility="no" />app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchHeaderAdapter.kt (2)
21-23: getItemViewType에서 SearchAllFragment 상수 의존은 불필요한 결합입니다. 메서드 자체 제거 권장.
이 어댑터는 단일 타입·단일 아이템이라 viewType 오버라이드가 무의미하고, Fragment의 상수에 묶이면 변경 취약해집니다.- override fun getItemViewType(position: Int): Int { - return SearchAllFragment.HEADER_VIEW_TYPE - }추가로 상단 import도 제거하세요:
-import com.example.findu.presentation.ui.search.tablayout.SearchAllFragment
1-38: SearchHeaderAdapter 제거 및 SearchListAdapter(Header 포함) 사용 통일
SearchHeaderAdapter는 코드베이스 어디에서도 참조되지 않으며, SearchListAdapter.submitContent()/addContent()에서 이미 헤더를 처리합니다. 중복되는 SearchHeaderAdapter.kt 파일과 관련 import를 삭제하고 SearchListAdapter만 사용하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
app/src/main/java/com/example/findu/presentation/ui/search/SearchSpacingItemDecoration.kt(1 hunks)app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchHeaderAdapter.kt(1 hunks)app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchListAdapter.kt(1 hunks)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt(6 hunks)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt(6 hunks)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt(5 hunks)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt(2 hunks)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt(2 hunks)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt(2 hunks)app/src/main/res/layout/fragment_search_all.xml(1 hunks)app/src/main/res/layout/fragment_search_report.xml(1 hunks)app/src/main/res/layout/fragment_search_rescue.xml(1 hunks)app/src/main/res/layout/item_search_header.xml(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchHeaderAdapter.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchListAdapter.kt (1)
onFilterClick(20-177)
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchListAdapter.kt (2)
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchHeaderAdapter.kt (1)
onFilterClick(9-38)app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchContentRVAdapter.kt (1)
onItemClick(14-146)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (2)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (6)
initRVAdapter(171-201)initDummyItems(54-113)setupRV(115-136)navigateToFilter(167-169)toggleLayoutMode(203-225)navigateToDetail(138-165)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (6)
initRVAdapter(202-231)initDummyItems(58-117)setupRV(119-140)navigateToFilter(198-200)toggleLayoutMode(234-256)navigateToDetail(142-169)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (2)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (6)
setupMap(82-106)initListener(162-177)initTagView(206-213)initBookmarkUI(198-204)openNaverMap(224-249)updateBookmarkUI(252-257)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (8)
setupMap(81-105)initListener(189-203)initViewPager(161-178)initTagView(213-220)initDummyImages(107-114)initBookmarkUI(205-211)openNaverMap(231-256)updateBookmarkUI(258-263)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (9)
setupMap(84-108)initDummyImages(110-117)observeViewModel(129-143)initListener(196-214)initViewPager(176-193)initTagView(224-231)initBookmarkUI(216-222)openNaverMap(242-267)updateBookmarkUI(270-275)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (2)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (6)
initRVAdapter(206-235)initDummyItems(59-118)setupRV(148-169)navigateToFilter(201-203)toggleLayoutMode(237-259)navigateToDetail(171-198)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (6)
initRVAdapter(202-231)initDummyItems(58-117)setupRV(119-140)navigateToFilter(198-200)toggleLayoutMode(234-256)navigateToDetail(142-169)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (2)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (6)
initRVAdapter(171-201)initDummyItems(54-113)setupRV(115-136)navigateToFilter(167-169)toggleLayoutMode(203-225)navigateToDetail(138-165)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (6)
initRVAdapter(206-235)initDummyItems(59-118)setupRV(148-169)navigateToFilter(201-203)toggleLayoutMode(237-259)navigateToDetail(171-198)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (5)
setupMap(84-108)initBookmarkUI(216-222)initListener(196-214)openNaverMap(242-267)updateBookmarkUI(270-275)
🪛 detekt (1.23.8)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
[warning] 104-104: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt
[warning] 101-101: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt
[warning] 102-102: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🔇 Additional comments (10)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (1)
63-66: id 파싱 안전화 적용 필요
Nav 그래프의id인자 타입을 확인한 후args.id.toLongOrNull() ?: -1L형태로 변환 방어 로직을 적용하세요.- cardId = args.id.ifBlank { cardId.toString() }.toLong() + cardId = args.id.toLongOrNull() ?: -1Lapp/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (1)
151-151: 날짜 표기 로캘 고정 확인(리뷰어 코멘트 대응)tvValueLostDate에 표시되는 문자열이 영어라면 포맷터에서 Locale.KOREA(또는 기기 Locale)를 명시적으로 사용했는지 확인 필요.
예: VM/매퍼에서 DateTimeFormatter.ofPattern("yyyy.MM.dd (E)", Locale.KOREA) 등으로 변환 후 바인딩.
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (2)
207-212: SearchListAdapter 초기화가 적절합니다콜백 함수들이 명확하게 정의되어 있고, 각각의 역할이 잘 분리되어 있습니다.
248-250: 그리드 모드에서 헤더 스팬 처리가 올바릅니다헤더(position 0)가 2개 컬럼에 걸쳐 표시되도록 SpanSizeLookup이 올바르게 구현되었습니다.
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (2)
172-179: SearchListAdapter 사용이 적절합니다SearchHeaderAdapter 대신 SearchListAdapter의 통합된 헤더/콘텐츠 방식을 사용하는 것이 더 효율적입니다.
227-229: HEADER_VIEW_TYPE 상수가 SearchHeaderAdapter와 일관성 있게 정의되었습니다다른 어댑터들과 호환될 수 있는 뷰 타입 상수를 제공하고 있습니다.
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (1)
174-196: Fragment Result 수신은 parentFragmentManager 쪽이 일반적입니다(네비게이션 목적지 기준 확인 필요)필터 화면이 같은 NavHost의 형제 목적지라면
parentFragmentManager.setFragmentResultListener가 더 안전합니다. child를 쓰면 결과를 못 받을 수 있어요. 현재는 동작 확인이 필요합니다.검토 제안:
- childFragmentManager.setFragmentResultListener( + parentFragmentManager.setFragmentResultListener( FILTER_RESULTS, viewLifecycleOwner ) { _, bundle -> ... }필터 프래그먼트에서 setFragmentResult를 어느 FragmentManager에 쏘는지도 함께 확인해 주세요.
app/src/main/java/com/example/findu/presentation/ui/search/SearchSpacingItemDecoration.kt (1)
19-26: 헤더(0번) 여백 0 처리, NO_POSITION 가드 둘 다 적절합니다.
스크롤 중 레이아웃 변동 시 크래시·깜빡임 방지에 유효해요.app/src/main/res/layout/fragment_search_report.xml (1)
10-20: 레이아웃 제약 구성 좋아요.
0dp(match-constraints) + Top/Bottom 제약 구성으로 RV가 안정적으로 확장됩니다.app/src/main/res/layout/fragment_search_rescue.xml (1)
10-20: 검증 완료: 기존 ID 잔존 없음
모든rv_search_rescue_horizontal_content참조가 코드베이스에서 제거되었습니다.
.../main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt
Show resolved
Hide resolved
| private var lastProtectId = Long.MAX_VALUE | ||
| private var isNewList = false | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
lastProtectId 업데이트 경로 확인 필요
페이징에 쓰는 lastProtectId가 어디서 갱신되는지 보이지 않습니다. 데이터 수신 시 마지막 아이템 id로 갱신하지 않으면 다음 페이지 요청이 반복되거나 멈출 수 있어요.
아래 스크립트로 레포에서 갱신 지점 존재 여부를 확인해 주세요:
Also applies to: 186-195
🏁 Script executed:
#!/bin/bash
# lastProtectId 갱신/참조 위치 확인
rg -n -C2 'lastProtectId' --type=ktLength of output: 26
ripgrep가 --type=kt를 인식하지 못해 검색이 안 됐습니다. .kt 파일에서 lastProtectId를 찾도록 glob 지정하고 다시 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# lastProtectId 갱신/참조 위치 재검색
rg -n -C2 'lastProtectId' --glob '*.kt'Length of output: 29027
lastProtectId 갱신 로직 추가 필요
- SearchRescueFragment.kt 186–195줄의 onSuccess 콜백에서 API 응답(SearchData)의 lastProtectId 값을 fragment의 lastProtectId에 할당해야 다음 페이지 요청 시 올바른 ID가 전달됩니다.
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt
around lines 39–41 and the onSuccess callback at lines 186–195, the fragment's
lastProtectId is never updated from the API response; modify the onSuccess
handling to assign the SearchData response's lastProtectId to this.lastProtectId
(ensuring proper nullable checks/casting if needed) so subsequent page requests
send the correct ID, and keep existing isNewList logic unchanged.
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt
Show resolved
Hide resolved
| binding.rvSearchRescue.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||
| override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||
| super.onScrolled(recyclerView, dx, dy) | ||
|
|
||
| val rvPosition = when (recyclerView.layoutManager) { | ||
| is LinearLayoutManager -> { | ||
| (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() | ||
| } | ||
|
|
||
| else -> { | ||
| (recyclerView.layoutManager as GridLayoutManager).findLastVisibleItemPosition() | ||
| } | ||
| val lastPos = when (val manager = recyclerView.layoutManager) { | ||
| is LinearLayoutManager -> manager.findLastVisibleItemPosition() | ||
| else -> return | ||
| } | ||
|
|
||
| val totalCount = recyclerView.adapter?.itemCount?.minus(1) ?: 0 | ||
| // 페이징 처리 | ||
| if (rvPosition == totalCount) { | ||
| viewModel.getSearchProtectData( | ||
| lastProtectId, | ||
| ) | ||
| val total = (recyclerView.adapter?.itemCount ?: 1) - 1 | ||
| //페이징 처리 | ||
| if (lastPos == total) { | ||
| viewModel.getSearchReportData(lastProtectId) | ||
| } | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
페이징 시 API 오타 및 GridLayoutManager 미처리로 무한 스크롤이 동작하지 않습니다
- Rescue 탭인데
getSearchReportData호출 중입니다. protect용 API로 교체 필요. - Grid 전환 시 현재 분기에서
GridLayoutManager를 처리하지 않아 페이징이 멈춥니다.
수정 예:
- val lastPos = when (val manager = recyclerView.layoutManager) {
- is LinearLayoutManager -> manager.findLastVisibleItemPosition()
- else -> return
- }
+ val lastPos = when (val manager = recyclerView.layoutManager) {
+ is LinearLayoutManager -> manager.findLastVisibleItemPosition()
+ is GridLayoutManager -> manager.findLastVisibleItemPosition()
+ else -> return
+ }
@@
- if (lastPos == total) {
- viewModel.getSearchReportData(lastProtectId)
- }
+ if (lastPos == total) {
+ viewModel.getSearchProtectData(lastProtectId)
+ }📝 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.rvSearchRescue.addOnScrollListener(object : RecyclerView.OnScrollListener() { | |
| override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | |
| super.onScrolled(recyclerView, dx, dy) | |
| val rvPosition = when (recyclerView.layoutManager) { | |
| is LinearLayoutManager -> { | |
| (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() | |
| } | |
| else -> { | |
| (recyclerView.layoutManager as GridLayoutManager).findLastVisibleItemPosition() | |
| } | |
| val lastPos = when (val manager = recyclerView.layoutManager) { | |
| is LinearLayoutManager -> manager.findLastVisibleItemPosition() | |
| else -> return | |
| } | |
| val totalCount = recyclerView.adapter?.itemCount?.minus(1) ?: 0 | |
| // 페이징 처리 | |
| if (rvPosition == totalCount) { | |
| viewModel.getSearchProtectData( | |
| lastProtectId, | |
| ) | |
| val total = (recyclerView.adapter?.itemCount ?: 1) - 1 | |
| //페이징 처리 | |
| if (lastPos == total) { | |
| viewModel.getSearchReportData(lastProtectId) | |
| } | |
| } | |
| }) | |
| } | |
| binding.rvSearchRescue.addOnScrollListener(object : RecyclerView.OnScrollListener() { | |
| override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | |
| super.onScrolled(recyclerView, dx, dy) | |
| val lastPos = when (val manager = recyclerView.layoutManager) { | |
| is LinearLayoutManager -> manager.findLastVisibleItemPosition() | |
| is GridLayoutManager -> manager.findLastVisibleItemPosition() | |
| else -> return | |
| } | |
| val total = (recyclerView.adapter?.itemCount ?: 1) - 1 | |
| // 페이징 처리 | |
| if (lastPos == total) { | |
| viewModel.getSearchProtectData(lastProtectId) | |
| } | |
| } | |
| }) |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt
around lines 217 to 231, the scroll listener calls the wrong API and doesn't
handle GridLayoutManager causing infinite scroll to fail; replace the
getSearchReportData(...) call with the correct protect API (e.g.,
getSearchProtectData(lastProtectId)) and extend the layoutManager when-check to
handle GridLayoutManager (use its findLastVisibleItemPosition() the same way as
LinearLayoutManager) so the lastPos computation works for both layouts before
triggering paging.
|
고생하셨습니다! UI 쪽에서 살짝 씩의 수정사항이 있을 것 같은데, QA 때 진행하시죠! |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (1)
151-161: 지도 주소 바인딩 불일치로 카메라 이동/네이버맵 열기 실패.updateUI는 tvWitnessLocation에 세팅하지만, 지도/클릭은 tvValueWitnessLocation을 읽습니다. 동일 뷰를 사용하도록 통일하세요.
수정안(주소를 tvValueWitnessLocation으로 일원화):
- tvWitnessLocation.text = data.eventLocation + tvValueWitnessLocation.text = data.eventLocation @@ - val address = binding.tvValueWitnessLocation.text.toString() + val address = binding.tvValueWitnessLocation.text.toString() @@ - val address = binding.tvValueWitnessLocation.text.toString() + val address = binding.tvValueWitnessLocation.text.toString()(만약 레이아웃에서 tvWitnessLocation만 존재한다면, setupMap()/llViewMap에서도 해당 뷰를 참조하도록 바꾸세요.)
Also applies to: 83-86, 196-199
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (1)
70-73: SafeArgs id(string) toLongOrNull 방어 로직 적용
args.id(argType=string)이므로 toLong() 호출 시 NumberFormatException 위험 — toLongOrNull()으로 방어해야 합니다.수정 제안 (app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt:70–73):
- cardId = args.id.ifBlank { cardId.toString() }.toLong() + cardId = args.id.toLongOrNull() ?: cardIdapp/src/main/res/layout/fragment_search_detail_witness.xml (1)
17-24: 아이콘 접근성/터치 피드백 보강백 버튼/북마크/복사/하단 액션에 콘텐츠 설명과 리플 효과를 추가하면 접근성과 클릭 피드백이 좋아집니다.
아래처럼 최소 속성만 추가 권장합니다:
@@ <ImageView android:id="@+id/iv_search_detail_back" + android:contentDescription="@string/a11y_back" /> @@ <ImageView android:id="@+id/iv_search_detail_bookmark" + android:contentDescription="@string/a11y_bookmark" /> @@ <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/cl_witness_location_copy" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackgroundBorderless"> @@ - <ImageView + <ImageView android:id="@+id/iv_search_copy" + android:contentDescription="@null" /> @@ <LinearLayout android:id="@+id/ll_send_message" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground" /> @@ <LinearLayout android:id="@+id/ll_view_map" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground" />추가로 strings.xml에 아래 리소스가 없으면 생성 부탁드립니다:
<string name="a11y_back">뒤로가기</string> <string name="a11y_bookmark">북마크</string>Also applies to: 27-37, 246-274, 379-401, 408-430
♻️ Duplicate comments (14)
app/src/main/res/drawable/bg_search_detail_bottom.xml (1)
4-11: 이전 피드백 반영됨 — 레이어 순서/다크테마 대응 LGTM베이스를
?attr/colorSurface로 먼저 그리고, 상단 라운드(23dp) 적용까지 일치합니다. 과거 코멘트의 가려짐 이슈가 해소됐고 다크테마에도 안전해요.app/src/main/res/layout/fragment_search_detail_protecting.xml (1)
230-230: 문자열 리소스 활용 👍특이사항 라벨과 값이 string 리소스로 잘 이동되었네요! 하드코딩 문제가 해결되었습니다.
Also applies to: 241-241
app/src/main/res/layout/fragment_search_all.xml (1)
9-20: RecyclerView 설정 개선됨 ✨이전 리뷰에서 지적된 문제들이 잘 해결되었네요:
- width가
0dp로 변경되어 responsive 레이아웃 지원- bottom constraint가 제대로 설정됨
- tools:listitem 추가로 디자인 타임 프리뷰 개선
app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt (1)
360-367: 드롭다운 충돌 방지 구현 완료 👍이전 리뷰에서 제안한 대로
toggleRecyclerViewVisibility에서 품종 드롭다운을 닫도록 수정되었습니다. UI 겹침 문제가 해결되었네요!app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (2)
50-51: 실제 데이터 로딩 비활성화됨observeViewModel()과 viewModel.getSearchAllData() 호출이 주석 처리되어 실제 데이터 로딩이 안 됩니다. 더미 데이터만 표시되고 있어요.
initRVAdapter() + observeViewModel() + viewModel.getSearchAllData() - initDummyItems() - setupRV(items) + if (BuildConfig.DEBUG) { + initDummyItems() + setupRV(items) + }
56-61: 더미 데이터 프로덕션 노출 위험프로덕션 빌드에서도 더미 데이터가 포함될 수 있습니다. DEBUG 빌드에서만 실행되도록 보호가 필요합니다.
@SuppressLint("VisibleForTests") private fun initDummyItems() { + if (!BuildConfig.DEBUG) return items.addAll(DummyProvider.getDummyAnimals()) items.addAll(DummyProvider.getDummyAnimals()) items.addAll(DummyProvider.getDummyAnimals()) }app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (3)
122-143: 필터 적용 후 페이징 기준 ID 갱신 여부 확인.lastProtectId를 응답의 마지막 아이템 기준으로 갱신하지 않으면 다음 페이지 요청이 꼬일 수 있어요. onSuccess 시점 갱신 로직 유무 확인 부탁드립니다.
확인 스크립트:
#!/bin/bash rg -n 'lastProtectId' --glob '*.kt' -C2
52-56: 데이터 흐름이 더미/실데이터 혼재(관찰 주석 + 더미 주입).관찰/요청이 주석 처리된 상태에서 더미 리스트만 세팅되어 필터/페이징 시 목록이 비는 문제가 재발할 수 있어요. DEBUG일 때만 더미 사용하고 기본은 VM 관찰로 일원화하세요.
예시 패치:
_binding = FragmentSearchRescueBinding.inflate(inflater, container, false) initRVAdapter() -// observeViewModel() -// viewModel.getSearchReportData() - initDummyItems() - setupRV(items) + observeViewModel() + viewModel.getSearchProtectData() + if (com.example.findu.BuildConfig.DEBUG) { + initDummyItems() + setupRV(items) + }
165-176: 무한 스크롤 오작동: Grid 미처리 + 잘못된 API 호출.
- GridLayoutManager 분기가 없어 Grid 모드에서 페이징 트리거가 되지 않습니다.
- Rescue 탭인데 getSearchReportData 호출 중입니다.
수정안:
- val lastPos = when (val manager = recyclerView.layoutManager) { - is LinearLayoutManager -> manager.findLastVisibleItemPosition() - else -> return - } + val lastPos = when (val manager = recyclerView.layoutManager) { + is LinearLayoutManager -> manager.findLastVisibleItemPosition() + is GridLayoutManager -> manager.findLastVisibleItemPosition() + else -> return + } @@ - if (lastPos == total) { - viewModel.getSearchReportData(lastProtectId) - } + if (lastPos == total) { + viewModel.getSearchProtectData(lastProtectId) + }app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (1)
262-291: MapView 라이프사이클 위임 추가 👍누수/흰 화면 방지에 도움됩니다.
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (3)
142-163: 지도·외부지도 주소 바인딩 누락 — 맵이 비어 보입니다tvValueProtectLocation을 세팅하지 않아 setupMap/openNaverMap이 빈 주소를 사용합니다. careAddr(또는 요구되는 실제 필드)로 채워 주세요.
tvShelterLocation.text = data.careAddr + tvValueProtectLocation.text = data.careAddr
255-284: onSaveInstanceState 누락(MapView 상태 보존)프로세스 재생성 시 맵 상태 유실됩니다. 상태 저장 위임을 추가하세요.
+ override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.mapView.onSaveInstanceState(outState) + }
150-150: 중성화 정보 매핑 수정
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt 라인 150에서 tvValueNeuter.text를 data.neutering으로 변경하세요.- tvValueNeuter.text = data.happenDate + tvValueNeuter.text = data.neuteringapp/src/main/res/layout/fragment_search_detail_witness.xml (1)
351-357: 목격 날짜 값 리소스 교체 (이전 코멘트와 동일)목격 컨텍스트인데 발견일 값 리소스를 참조하고 있습니다. witness 전용 값/바인딩으로 교체해주세요.
-android:text="@string/search_detail_found_date_value" +android:text="@string/search_witness_date_value"
🧹 Nitpick comments (40)
app/src/main/res/drawable/bg_search_detail_bottom.xml (2)
13-22: 그라데이션 색을 테마 기반으로 전환해 다크/다이내믹 컬러 대응 강화현재
#33000000(반투명 블랙) 고정이라 일부 테마에서 대비가 과하거나 부족할 수 있어요. 시작색을?attr/colorOnSurface로 두고 아이템 레벨 알파를 주면 테마 변화에 더 자연스럽게 따라갑니다.- <item android:height="8dp"> + <item android:height="8dp" android:alpha="0.2"> <shape android:shape="rectangle"> <corners android:topLeftRadius="23dp" android:topRightRadius="23dp" /> <gradient android:angle="270" - android:endColor="#00000000" - android:startColor="#33000000" /> + android:endColor="@android:color/transparent" + android:startColor="?attr/colorOnSurface" /> </shape> </item>참고: 알파 값(여기선 0.2)은 화면에서 살짝만 어두워지도록 조절하되, 실제 기기에서 라이트/다크 모두 확인 부탁드립니다.
7-9: 매직 넘버(23dp/8dp) dimen 추출 제안반경과 페이드 높이를
@dimen으로 뽑아두면 레이아웃 전반의 곡률/컴포넌트 간 일관성이 좋아집니다. 추후 디자인 토큰 변경도 쉬워요.- android:topLeftRadius="23dp" - android:topRightRadius="23dp" /> + android:topLeftRadius="@dimen/shape_corner_large" + android:topRightRadius="@dimen/shape_corner_large" /> ... - <item android:height="8dp"> + <item android:height="@dimen/search_detail_top_fade_height"> ... - android:topLeftRadius="23dp" - android:topRightRadius="23dp" /> + android:topLeftRadius="@dimen/shape_corner_large" + android:topRightRadius="@dimen/shape_corner_large" />없다면
values/dimens.xml에 아래처럼 추가하는 걸 권장합니다:
- shape_corner_large = 24dp(혹은 현재 UI 기준 23dp 유지)
- search_detail_top_fade_height = 8dp
Also applies to: 16-17, 13-13
app/src/main/res/layout/fragment_search_detail_protecting.xml (1)
358-358: 하드코딩된 색상 값 발견
android:textColor="#00D1CA"로 색상이 하드코딩되어 있습니다. 일관성 유지를 위해 color 리소스로 이동하는 게 좋겠습니다.- android:textColor="#00D1CA" + android:textColor="@color/main_color_variant"색상 리소스 파일에 추가:
<color name="main_color_variant">#00D1CA</color>app/src/main/java/com/example/findu/presentation/ui/search/model/SearchDummyProvider.kt (1)
8-44: 더미 데이터가 프로덕션 빌드에서도 포함됨현재 구조상
DummyProvider가 프로덕션 빌드에도 포함됩니다. Line 12에서BuildConfig.DEBUG체크는 있지만, 클래스 자체는 여전히 APK에 포함되어 앱 크기를 늘립니다.다음 중 하나를 선택하세요:
옵션 1: debug 소스셋으로 이동
# 파일을 debug 전용 디렉토리로 이동 mkdir -p app/src/debug/java/com/example/findu/presentation/ui/search/model/ mv app/src/main/java/com/example/findu/presentation/ui/search/model/SearchDummyProvider.kt \ app/src/debug/java/com/example/findu/presentation/ui/search/model/옵션 2: ProGuard/R8 규칙 추가로 릴리즈 빌드에서 제거
-assumenosideeffects class com.example.findu.presentation.ui.search.model.DummyProvider { public static *** getDummyAnimals(...); }app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt (1)
132-133: 빈 TextWatcher 콜백 메서드static analysis에서 지적한 빈 메서드들입니다. 이런 경우엔 익명 클래스 대신 람다를 활용할 수 있습니다.
- actvSearchFilterBreed.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun afterTextChanged(s: Editable?) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + actvSearchFilterBreed.doOnTextChanged { s, start, before, count -> if (suppressBreedTextWatcher || !actvSearchFilterBreed.isEnabled || !this@SearchFilterFragment::breedRvAdapter.isInitialized) { setBreedDropdown(false); return } breedRvAdapter.filter.filter(token()) setBreedDropdown(true) - } - }) + }import 추가 필요:
import androidx.core.widget.doOnTextChangedapp/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (1)
61-66: 더미 데이터 사용 시 DEBUG 가드 필요
@VisibleForTesting어노테이션이 있지만 프로덕션에서도 더미 데이터가 로드됩니다. BuildConfig.DEBUG 체크를 추가하는 게 안전합니다.@SuppressLint("VisibleForTests") private fun initDummyItems() { + if (!BuildConfig.DEBUG) return items.addAll(DummyProvider.getDummyAnimals()) items.addAll(DummyProvider.getDummyAnimals()) items.addAll(DummyProvider.getDummyAnimals()) }app/src/main/res/values/strings.xml (6)
108-109: 라벨과 값의 의미 불일치(기간 vs 날짜).값이 기간(2024.11.23 ~ 2024.12.03)인데 라벨은 “공고 날짜”로 되어 있어요. “공고 기간”이 더 정확합니다.
적용 예:
- <string name="search_detail_noti_date">공고 날짜</string> + <string name="search_detail_noti_date">공고 기간</string>
102-102: 불리언/상태값 로컬라이즈 필요.“Y”는 영문 약어입니다. 사용자 노출용이면 “예/아니요” 등 한글로 통일하는 게 좋아요. 데이터 바인딩에서 변환해도 됩니다.
예시:
- <string name="search_detail_neuter_value">Y</string> + <string name="search_detail_neuter_value">예</string>
125-126: 행정구역 표기 순서 보정 제안.통상 “시/군/구” 순으로 표기합니다. 현재 “시/구/군”이라 혼동 여지가 있어요.
- <string name="search_filter_location_gu">시/구/군 선택</string> + <string name="search_filter_location_gu">시/군/구 선택</string>
431-432: 구분자 리소스 중복 통합.slash, search_filter_divider 모두 “/”입니다. 한 개만 유지하고 참조 통일하면 좋아요.
Also applies to: 458-459
81-81: 실제 주소/PII 샘플 사용 주의.실 배포용이면 실존지처럼 보이는 상세주소는 마스킹 권장(예: “OO시 OO구 OO로 123-4”).
원하시면 일괄 마스킹 스크립트 드릴게요.
66-67: 날짜 구분자 통일
리소스 내 점(.)과 엔 대시(–)가 혼용되어 있으니, 하나의 구분자로 통일하거나date_single/date_range포맷 문자열로 대체하세요.app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (2)
80-85: 0번 포지션 이중 스크롤 호출 제거 제안.scrollToPosition(0) 직후 smoothScrollToPosition(0)은 중복입니다. 하나만 유지하세요.
- binding.rvSearchRescue.scrollToPosition(0) - binding.rvSearchRescue.smoothScrollToPosition(0) + binding.rvSearchRescue.scrollToPosition(0)
186-203: 그리드/리니어 전환 시 ItemDecoration 정리.그리드 진입 시 데코 추가만 있고 리니어 복귀 시 제거되지 않습니다. 모드 전환마다 데코를 일관되게 관리하세요.
예:
if (isGridMode) { while (binding.rvSearchRescue.itemDecorationCount > 0) { binding.rvSearchRescue.removeItemDecorationAt(0) } binding.rvSearchRescue.addItemDecoration(SearchSpacingItemDecoration(10)) @@ } else { - binding.rvSearchRescue.layoutManager = + while (binding.rvSearchRescue.itemDecorationCount > 0) { + binding.rvSearchRescue.removeItemDecorationAt(0) + } + binding.rvSearchRescue.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) listAdapter.setGridMode(false) }app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (3)
107-111: 로그 태그 오탈자.“SearchDisappearDetail” → “SearchWitnessDetail”.
- Log.w("SearchDisappearDetail", "Geocoding failed", e) + Log.w("SearchWitnessDetail", "Geocoding failed", e)
186-194: 토스트 문자열 리소스화."준비 중이에요!" 하드코딩 대신 strings.xml 사용 권장(다국어/일관성).
- Toast.makeText(requireContext(), "준비 중이에요!", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), getString(R.string.common_coming_soon), Toast.LENGTH_SHORT).show()(리소스 예: 준비 중이에요!)
151-155: 태그 텍스트 이중 세팅 정리.tvDetailTagField를 updateUI와 initTagView에서 중복으로 세팅합니다. 한 곳으로 일원화하세요.
- tvDetailTagField.text = convertTagToKorean(data.tag.text) @@ - val koreanTag = convertTagToKorean(data.tag.toString()) - binding.tvDetailTagField.text = koreanTag + val koreanTag = convertTagToKorean(data.tag.toString()) + binding.tvDetailTagField.text = koreanTag(위처럼 updateUI의 직접 세팅을 제거하고 initTagView만 유지)
Also applies to: 210-217
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (6)
50-51: 미사용 필드 정리lateinit mapView 필드는 사용되지 않습니다. binding.mapView만 사용하므로 제거하세요.
- private lateinit var mapView: MapView
98-103: 마커 높이 단위 보정(px ↔ dp)Marker.height는 px 단위입니다. 디바이스 독립적으로 dimens로 관리하세요.
- icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) - height = 23 + icon = OverlayImage.fromResource(R.drawable.ic_search_map_marker) + height = resources.getDimensionPixelSize(R.dimen.search_map_marker_height)추가: res/values/dimens.xml에 search_map_marker_height 정의(예: 24dp).
75-75: 사용자 노출 문자열 리소스화하드코딩된 토스트 문구를 strings.xml로 이동해 i18n/일관성 확보하세요.
- Toast.makeText(requireContext(), "잘못된 접근입니다.", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), getString(R.string.error_invalid_access), Toast.LENGTH_SHORT).show()- Toast.makeText(requireContext(), "준비 중이에요!", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), getString(R.string.search_detail_coming_soon), Toast.LENGTH_SHORT).show()Also applies to: 202-204
44-48: Fragment.tag와 이름 충돌 — 속성명 변경 권장Fragment의 내장 tag와 혼동됩니다. reportTag 등으로 변경하세요.
- private var tag: String? = null + private var reportTag: String? = null ... - tag = args.tag.ifBlank { tag } + reportTag = args.tag.ifBlank { reportTag } - if (cardId == -1L || tag == null) { + if (cardId == -1L || reportTag == null) {Also applies to: 71-75
194-212: 주소 복사 동작 연결XML에 복사 UI가 있으나 동작이 없습니다. 클릭 핸들러 추가하세요.
private fun initListener() = with(binding) { initDummyImages() @@ llViewMap.setOnClickListener { val address = binding.tvValueLostLocation.text.toString() openNaverMap(address) } + + clLostLocationCopy.setOnClickListener { + val text = binding.tvValueLostLocation.text.toString() + if (text.isNotBlank()) { + val cm = requireContext().getSystemService(android.content.ClipboardManager::class.java) + cm.setPrimaryClip(android.content.ClipData.newPlainText("location", text)) + Toast.makeText(requireContext(), getString(R.string.search_detail_copied), Toast.LENGTH_SHORT).show() + } + } }
275-304: MapView 라이프사이클 위임 추가된 부분 좋습니다onStart/onResume/onPause/onStop/onLowMemory/onDestroyView 모두 위임되어 안정적입니다. onSaveInstanceState만 추가하면 완벽합니다.
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (6)
145-146: 태그 뷰 이중 세팅 — 한 곳으로 단일화updateUI에서 텍스트 지정 후 initTagView에서 다시 지정합니다. 한 군데로 통일하세요.
- tvDetailTagField.text = convertTagToKorean(data.tag.text) ... initTagView(data)Also applies to: 202-209
105-110: 로그 태그 오타보호 상세 파일에서 "SearchDisappearDetail"을 사용하고 있습니다. 파일명과 맞춰 주세요.
- Log.w("SearchDisappearDetail", "Geocoding failed", e) + Log.w("SearchProtectingDetail", "Geocoding failed", e)
44-48: 미사용 필드 제거lateinit mapView는 사용되지 않습니다. 정리 권장.
- private lateinit var mapView: MapView
76-78: initBookmarkUI 중복 호출onViewCreated와 initListener에서 중복 호출됩니다. 한 곳만 남기세요.
- initBookmarkUI() initListener() @@ - initBookmarkUI()Also applies to: 171-172
39-40: Fragment.tag와 충돌 가능 — 변수명 변경 권장내장 tag와 혼동될 수 있으니 statusTag 등으로 리네이밍하세요.
- private var tag: String? = null + private var statusTag: String? = null
221-245: 중복 유틸 함수 통합openNaverMap 구현이 세 상세 프래그먼트에 중복됩니다. util/extension으로 추출해 재사용하세요.
app/src/main/res/layout/fragment_search_detail_disappear.xml (5)
26-37: 접근성: 북마크 아이콘에 contentDescription 추가스크린 리더 지원을 위해 설명을 넣어 주세요.
<ImageView android:id="@+id/iv_search_detail_bookmark" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:layout_marginEnd="21dp" android:layout_marginBottom="17dp" android:background="@drawable/bg_search_rounded_box" android:clickable="true" android:focusable="true" + android:contentDescription="@string/search_detail_bookmark_cd" android:src="@drawable/ic_search_detail_blank_bookmark"
316-344: ‘주소 복사’ 컨테이너 클릭 처리/리플/접근성UX 강화를 위해 클릭 가능, 포커스 가능, 리플, CD를 추가하세요.
- <androidx.constraintlayout.widget.ConstraintLayout + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/cl_lost_location_copy" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="3dp" + android:clickable="true" + android:focusable="true" + android:background="?attr/selectableItemBackgroundBorderless" app:layout_constraintBottom_toBottomOf="@id/tv_value_lost_location" app:layout_constraintStart_toEndOf="@id/tv_value_lost_location" app:layout_constraintTop_toTopOf="@id/tv_value_lost_location"> @@ - <ImageView + <ImageView android:id="@+id/iv_search_copy" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/img_search_detail_copy" + android:contentDescription="@string/search_detail_copy_cd"
335-343: 하드코딩 색상 제거직접 색상값 대신 자원 사용.
- android:textColor="#00D1CA" + android:textColor="@color/main_color" ``] --- `486-493`: **음수 마진 지양** 레이아웃 깨짐 가능성이 있어 패딩으로 대체를 권장합니다. ```diff - android:layout_marginStart="-10dp" + android:paddingStart="10dp"
256-273: MapView 섹션 추가 잘 반영됨라운드 클리핑과 전용 컨테이너 구성 모두 좋습니다. 상응하는 프래그먼트에서 라이프사이클 위임(onSaveInstanceState 포함)만 보완하면 완성입니다.
app/src/main/res/layout/fragment_search_detail_witness.xml (6)
264-273: 하드코딩 컬러 제거텍스트 컬러가 Hex로 고정되어 있습니다. 팔레트 일관성을 위해 color 리소스 참조로 교체하세요.
-android:textColor="#00D1CA" +android:textColor="@color/main_color"
75-79: 프리뷰 전용 텍스트는 tools:text로 이동런타임 데이터 바인딩 예정인 값들은 android:text 대신 tools:text로 옮겨두면 QA/디자인 프리뷰는 유지하면서 하드값 누수 방지됩니다.
-android:text="@string/search_name_welshcorgi" +tools:text="@string/search_name_welshcorgi" @@ -android:text="@string/search_detail_hair_color_value" +tools:text="@string/search_detail_hair_color_value" @@ -android:text="@string/search_detail_label_note_value" +tools:text="@string/search_detail_label_note_value" @@ -android:text="@string/search_detail_witness_location_value" +tools:text="@string/search_detail_witness_location_value" @@ -android:text="@string/search_detail_reporter_name_value" +tools:text="@string/search_detail_reporter_name_value"Also applies to: 126-133, 143-152, 235-243, 329-336
193-194: CircularFlow 속성 오용 가능성
app:circularflow_defaultRadius는 CircularFlow Helper 전용 속성입니다. 일반 ConstraintLayout에선 무시됩니다. 혼동 방지를 위해 제거 권장합니다.-android:clipToOutline="true" -app:circularflow_defaultRadius="20dp" +android:clipToOutline="true"
419-421: 네거티브 마진 지양아이콘 start에 -10dp는 레이아웃 안정성을 해칠 수 있습니다. 패딩/정렬로 대체하세요.
-android:layout_marginStart="-10dp" +android:layout_marginStart="0dp"필요 시 부모 LinearLayout에
android:paddingStart를 주거나 첫 TextView에 margin을 조정하는 방식으로 간격을 맞추는 걸 권장합니다.
197-203: MapView 스크롤/수명주기 주의ScrollView 내부 MapView는 제스처 충돌과 수명주기 호출 누락 이슈가 흔합니다.
- 제스처: 맵 컨테이너 터치 시 부모 인터셉트 해제
cl_search_map.setOnTouchListener { _, _ -> sv_search_detail.requestDisallowInterceptTouchEvent(true); false }- 또는
uiSettings.isScrollGesturesEnabled = false로 맵 스크롤만 끄기- 수명주기: Fragment에서
mapView.onCreate/onStart/onResume/onPause/onStop/onDestroy/onLowMemory위임 누락 여부 확인- 성능: 정적 미리보기라면 지도 스냅샷(Static Map) 고려
원하시면 Fragment 쪽 보일러플레이트도 만들어 드릴게요.
362-370: 바텀 카드 섹션 구조 좋습니다고정 카드 느낌 잘 납니다. Material 디자인 일관성을 더하려면 MaterialCardView로 전환하는 것도 선택지입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt(1 hunks)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt(6 hunks)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt(5 hunks)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt(5 hunks)app/src/main/java/com/example/findu/presentation/ui/search/dialog/SearchFilterDateDialog.kt(1 hunks)app/src/main/java/com/example/findu/presentation/ui/search/model/DummySearchData.kt(1 hunks)app/src/main/java/com/example/findu/presentation/ui/search/model/SearchDummyProvider.kt(1 hunks)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt(2 hunks)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt(2 hunks)app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt(2 hunks)app/src/main/res/drawable/bg_search_detail_bottom.xml(1 hunks)app/src/main/res/layout/fragment_search_all.xml(1 hunks)app/src/main/res/layout/fragment_search_detail_disappear.xml(3 hunks)app/src/main/res/layout/fragment_search_detail_protecting.xml(4 hunks)app/src/main/res/layout/fragment_search_detail_witness.xml(3 hunks)app/src/main/res/layout/fragment_search_filter.xml(1 hunks)app/src/main/res/values/strings.xml(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- app/src/main/java/com/example/findu/presentation/ui/search/model/DummySearchData.kt
- app/src/main/java/com/example/findu/presentation/ui/search/dialog/SearchFilterDateDialog.kt
- app/src/main/res/layout/fragment_search_filter.xml
🧰 Additional context used
🧬 Code graph analysis (7)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (6)
initRVAdapter(154-183)initDummyItems(61-66)setupRV(96-117)navigateToFilter(149-151)toggleLayoutMode(185-207)navigateToDetail(119-146)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (10)
initDummyImages(120-127)observeViewModel(139-153)fetchDetailData(129-137)initListener(194-212)initViewPager(174-191)setupMap(85-117)initTagView(222-229)initBookmarkUI(214-220)openNaverMap(240-265)updateBookmarkUI(268-273)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (2)
initTagView(222-229)openNaverMap(240-265)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (6)
initRVAdapter(119-149)initDummyItems(56-61)setupRV(63-84)navigateToFilter(115-117)toggleLayoutMode(151-173)navigateToDetail(86-113)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt (6)
initRVAdapter(119-149)initDummyItems(56-61)setupRV(63-84)navigateToFilter(115-117)toggleLayoutMode(151-173)navigateToDetail(86-113)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt (2)
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt (6)
initListener(167-182)initTagView(202-209)setupMap(81-112)initBookmarkUI(194-200)openNaverMap(220-245)updateBookmarkUI(248-253)app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (8)
initListener(186-200)initViewPager(167-184)initTagView(210-217)setupMap(83-114)initDummyImages(116-123)initBookmarkUI(202-208)openNaverMap(228-253)updateBookmarkUI(255-260)
app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt (1)
app/src/main/java/com/example/findu/presentation/ui/search/dialog/SearchFilterDateDialog.kt (1)
initListener(51-100)
🪛 detekt (1.23.8)
app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt
[warning] 132-132: This empty block of code can be removed.
(detekt.empty-blocks.EmptyFunctionBlock)
[warning] 133-133: This empty block of code can be removed.
(detekt.empty-blocks.EmptyFunctionBlock)
🔇 Additional comments (8)
app/src/main/java/com/example/findu/presentation/ui/search/SearchFilterFragment.kt (1)
283-293: 날짜 검증 로직 잘 구현됨시작 날짜 변경 시 종료 날짜가 시작 날짜보다 이전인 경우 자동으로 초기화하는 로직이 잘 구현되었습니다. 사용자 경험이 개선되었네요!
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchReportFragment.kt (1)
172-174: GridLayoutManager 케이스 누락스크롤 리스너에서 GridLayoutManager 처리가 빠져있습니다. SearchAllFragment와 동일하게 처리가 필요합니다.
val lastPos = when (val manager = recyclerView.layoutManager) { is LinearLayoutManager -> manager.findLastVisibleItemPosition() + is GridLayoutManager -> manager.findLastVisibleItemPosition() else -> return }Likely an incorrect or invalid review comment.
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt (3)
67-78: 아이템 매핑 로직은 명확하고 안정적입니다.
158-163: 애니메이터 비활성화로 깜빡임 방지 처리 좋습니다.
206-209: onDestroyView에서 어댑터 null 처리 OK.app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchWitnessDetailFragment.kt (2)
55-59: MapView 초기화 흐름 적절합니다(getMapAsync 분리).
83-114: 지오코딩 비동기 처리와 UI 스레드 전환 적절.IO에서 처리 후 Main으로 전환, 실패 처리 토스트 리소스 사용도 좋아요.
app/src/main/res/layout/fragment_search_detail_witness.xml (1)
43-46: ScrollView 제약 세팅 좋습니다0dp + fillViewport + top/bottom 제약 조합이 깔끔합니다. 스크롤 영역/바텀 섹션 겹침 이슈 없겠네요.
...c/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
Show resolved
Hide resolved
| // observeViewModel() | ||
| // fetchDetailData() |
There was a problem hiding this comment.
데이터 로딩이 비활성화됨 — 주석 해제 필요
상세 데이터가 로드되지 않아 화면이 더미 상태로 남습니다. 병합 전 반드시 활성화해 주세요.
-// observeViewModel()
-// fetchDetailData()
+ observeViewModel()
+ fetchDetailData()📝 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.
| // observeViewModel() | |
| // fetchDetailData() | |
| observeViewModel() | |
| fetchDetailData() |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
around lines 80-81, two calls are commented out (observeViewModel() and
fetchDetailData()), which prevents detail data from loading; uncomment these two
lines so the fragment registers observers and triggers the data fetch (ensure
they remain in the appropriate lifecycle method such as onViewCreated and that
no duplicate invocations occur).
...c/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
Show resolved
Hide resolved
.../main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt
Show resolved
Hide resolved
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchAllFragment.kt
Show resolved
Hide resolved
app/src/main/java/com/example/findu/presentation/ui/search/tablayout/SearchRescueFragment.kt
Show resolved
Hide resolved
| android:layout_marginStart="24dp" | ||
| android:layout_marginTop="30dp" | ||
| android:text="@string/search_name_welshcorgi" | ||
| app:layout_constraintStart_toStartOf="parent" |
There was a problem hiding this comment.
🛠️ Refactor suggestion
제목 더미 텍스트 제거 → tools:text로 이동
실 기기에도 고정 문자열이 노출됩니다. 프리뷰 전용으로 옮기세요.
- android:text="@string/search_name_welshcorgi"
+ tools:text="@string/search_name_welshcorgi"📝 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.
| android:layout_marginStart="24dp" | |
| android:layout_marginTop="30dp" | |
| android:text="@string/search_name_welshcorgi" | |
| app:layout_constraintStart_toStartOf="parent" | |
| android:layout_marginStart="24dp" | |
| android:layout_marginTop="30dp" | |
| tools:text="@string/search_name_welshcorgi" | |
| app:layout_constraintStart_toStartOf="parent" |
🤖 Prompt for AI Agents
In app/src/main/res/layout/fragment_search_detail_disappear.xml around lines
76-79, remove the runtime hardcoded android:text attribute (currently set to
@string/search_name_welshcorgi) and instead set a preview-only tools:text with
the same content; also ensure the root layout declares the tools namespace
(xmlns:tools="http://schemas.android.com/tools") if it's not already present so
the text appears only in the layout preview and not on real devices.
| <TextView | ||
| android:id="@+id/tv_value_age" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:text="@string/search_detail_age_value" | ||
| android:textAppearance="@style/TextAppearance.FindU.Body1_SB_16" | ||
| android:textColor="@color/gray6" /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
값 영역 더미 문자열 제거 → tools:text 사용
데이터 바인딩 실패 시 샘플 값이 사용자에게 노출됩니다.
- android:text="@string/search_detail_age_value"
+ tools:text="@string/search_detail_age_value"- android:text="@string/search_detail_report_date_value"
+ tools:text="@string/search_detail_report_date_value"- android:text="@string/search_detail_rfid_value"
+ tools:text="@string/search_detail_rfid_value"Also applies to: 173-179, 197-203
🤖 Prompt for AI Agents
In app/src/main/res/layout/fragment_search_detail_disappear.xml around lines
127-133 (and similarly at 173-179 and 197-203), the TextView currently uses
android:text with a dummy/sample string which will show to users if data binding
fails; remove the runtime android:text attribute and replace it with tools:text
so the sample value is only used in layout preview, keeping textAppearance and
textColor unchanged; ensure no runtime default text remains and verify
data-binding expressions remain intact for those TextViews.
| <string name="search_detail_shelter_phone_num">전화번호</string> | ||
| <string name="search_detail_shelter_phone_num_value">010–2868–2108</string> |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
전화번호 구분 기호(–)는 하이픈(-)으로.
en dash(–)는 다이얼러 인식 이슈가 있을 수 있어요. 하이픈(-) 권장.
아래 스크립트로 en dash 사용 위치를 전체 리소스에서 점검해 주세요.
수정 예:
- <string name="search_detail_shelter_phone_num_value">010–2868–2108</string>
+ <string name="search_detail_shelter_phone_num_value">010-2868-2108</string>🏁 Script executed:
#!/bin/bash
rg -n --type xml '–' app/src/main/res/valuesLength of output: 1354
리소스 문자열 en dash(–) → 하이픈(-) 일괄 교체
app/src/main/res/values/strings.xml에서 en dash(–) 사용(총 12곳)을 모두 하이픈(-)으로 변경하세요.
예시 diff:
- <string name="search_detail_shelter_phone_num_value">010–2868–2108</string>
+ <string name="search_detail_shelter_phone_num_value">010-2868-2108</string>📝 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.
| <string name="search_detail_shelter_phone_num">전화번호</string> | |
| <string name="search_detail_shelter_phone_num_value">010–2868–2108</string> | |
| <string name="search_detail_shelter_phone_num">전화번호</string> | |
| <string name="search_detail_shelter_phone_num_value">010-2868-2108</string> |
🤖 Prompt for AI Agents
In app/src/main/res/values/strings.xml around lines 114-115, several string
resource values use the en dash character (–) instead of the ASCII hyphen (-);
locate all occurrences (12 total) of the en dash within string values and
replace each with a hyphen, keeping the rest of the XML intact and preserving
spacing and encoding; verify changes compile and run lint/gradle build to ensure
no other resource keys or values were altered.
t1nm1ksun
left a comment
There was a problem hiding this comment.
코드량이 많은데 고생 많으셨습니다!
제가 처음부터 보던 코드가 아니기도하고 xml이라 질문몇개 남겼습니다...ㅠ
| private var visibleItems = allItems.toMutableList() | ||
|
|
||
| override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||
| val b = ItemSearchBreedsBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
There was a problem hiding this comment.
화면에 보여지는 list입니다. 초기에 모든 항목 표시하도록 초기화 해주고 필터링되면 값이 갱신됩니다
| @SuppressLint("NotifyDataSetChanged") | ||
| @Suppress("UNCHECKED_CAST") |
There was a problem hiding this comment.
이친구들은 어떤 역할인가요??
혹시 ListAdapter + DiffUtil 에 대해 알고계신가요?
There was a problem hiding this comment.
값 바뀌면 리싸이클러뷰 아이템 다시 그리도록 했는데 성능적으로 비효율인거 같습니다.
ListAdapter + DiffUtil 사용해서 필요한 부분만 업데이트 하도록 수정해야겠네요...
| import com.example.findu.presentation.ui.search.model.DetailSearchRv | ||
|
|
||
| class SearchDetailVPAdapter(private val images:List<String>) : | ||
| class SearchDetailVPAdapter(private val images:List<Any>) : |
There was a problem hiding this comment.
Any 말고 가독성 측면에서 더 좋은 타입도 있을 것 같아요~
실제 들어오게 되는 클래스나 제네릭...?
| Glide.with(binding.root) | ||
| .load(item) | ||
| .into(binding.ivSearchDetailContent) | ||
| } |
There was a problem hiding this comment.
Coil이 아닌 Glide를 사용하신 이유가 궁금합니다!
There was a problem hiding this comment.
제가 전부터 Glide를 거의 써와서.. 그냥 글라이드 쓴거같습니다.....
| private val onFilterClick: () -> Unit, | ||
| private val onToggleClick: () -> Unit, | ||
| private val onItemClick: (SearchRv) -> Unit, | ||
| private val onBookmarkClick: (Long, Boolean, String) -> Unit |
There was a problem hiding this comment.
Long, Booelan, String 이 무얼 나타내는지 궁금합니다! -> 자주 쓰이는 조합(특정 객체를 나타내는 경우)이라면 하나의 데이터 클래스로 묶는건 어떨까요
...c/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
Show resolved
Hide resolved
.../main/java/com/example/findu/presentation/ui/search/detail/SearchProtectingDetailFragment.kt
Show resolved
Hide resolved
|
|
||
| private fun fetchDetailData() { | ||
| when (tag) { | ||
| "목격신고", "실종신고" -> viewModel.getDetailSearchReport(cardId) |
app/src/main/java/com/example/findu/presentation/ui/search/adapter/SearchBreedRVAdapter.kt
Show resolved
Hide resolved
...c/main/java/com/example/findu/presentation/ui/search/detail/SearchDisappearDetailFragment.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/res/layout/fragment_search_all.xml (1)
2-7: root ConstraintLayout에 android:layout_height 누락 (화면 전체 높이 미확정).Fragment 루트가 wrap_content로 떨어져 RV가 0dp 제약을 풀로 소비하지 못할 수 있습니다. match_parent로 고정하세요.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/fragment_search_all" - android:layout_width="match_parent" + android:layout_width="match_parent" + android:layout_height="match_parent"
🧹 Nitpick comments (2)
app/src/main/res/layout/fragment_search_all.xml (2)
12-14: 여백 정책 정렬(4dp 그리드/Material 간격).top 15dp, bottom 10dp는 그리드가 어긋납니다. 16/12dp 등 규격화된 값 또는 dimens 참조로 통일을 권장합니다.
12-14: RV margin 대신 padding+clipToPadding=false 고려.양 옆 20dp 마진은 에지 글로우/스와이프 경험을 줄일 수 있습니다. 아이템 인셋은 ItemDecoration 또는 RV padding으로 처리하는 방식도 검토해 주세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
app/build.gradle.kts(1 hunks)app/src/main/res/layout/fragment_search_all.xml(1 hunks)app/src/main/res/navigation/nav_graph.xml(2 hunks)app/src/main/res/values/strings.xml(4 hunks)gradle/libs.versions.toml(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- app/src/main/res/navigation/nav_graph.xml
- gradle/libs.versions.toml
- app/build.gradle.kts
🔇 Additional comments (8)
app/src/main/res/values/strings.xml (5)
70-71: 날짜 포맷 변경 확인됨en dash는 날짜 범위에서 "(up) to and including"을 의미할 때 사용되지만, 일반적으로 UI에서는 점(.)이 더 일반적입니다. 변경사항이 적절해 보입니다.
118-119: 전화번호 en dash(–) → 하이픈(-) 수정 필요전화번호의 하이픈은 읽기 쉽게 하기 위한 것이며 다이얼링에는 영향을 주지 않지만, 하이픈은 긴 숫자 문자열에서 오류를 줄이는 데 도움이 됩니다. 현재 en dash(–) 사용은 이전 리뷰에서 지적된 문제입니다.
이전 리뷰 코멘트에서 지적한 대로 전화번호의 en dash(–)를 하이픈(-)으로 변경해 주세요:
- <string name="search_detail_shelter_phone_num_value">010–2868–2108</string> + <string name="search_detail_shelter_phone_num_value">010-2868-2108</string>
123-125: 필터 날짜 입력 필드 개선날짜 선택 UI의 확장성을 고려한 좋은 변경입니다. 시작/종료 날짜를 분리하여 범위 선택을 명확히 했네요.
129-131: 위치 선택 UI 텍스트 개선"시/도 선택", "시/구/군 선택"으로 변경하여 사용자에게 더 명확한 안내를 제공하고, 품종 입력 힌트도 "선택하거나 입력"으로 개선하여 다중 입력 방식을 안내한 점이 좋습니다.
466-497: 확인: 새 문자열 리소스가 레이아웃에서 참조됩니다.
다음 파일들에서 참조가 발견되었습니다:
- app/src/main/res/layout/fragment_search_filter.xml
- app/src/main/res/layout/fragment_search_detail_witness.xml
- app/src/main/res/layout/fragment_search_detail_protecting.xml
- app/src/main/res/layout/fragment_search_detail_disappear.xml
app/src/main/res/layout/fragment_search_all.xml (3)
9-19: RV 제약·폭 설정 수정 LGTM.width 0dp와 top/bottom 제약 보완으로 이전 랜더링 이슈 해결되었습니다.
9-10: ID 리네임 전역 반영 여부 확인 (rv_search_all_horizontal_content → rv_search_all)app/src/main/res/layout/fragment_search_all.xml에서는 android:id="@+id/rv_search_all"로 변경된 것을 확인했습니다. 그러나 레포 전역 검색이 일부 필터로 누락되어 기존 ID 참조(rv_search_all_horizontal_content / R.id.rv_search_all_horizontal_content)의 잔여 여부를 확정할 수 없습니다. 로컬에서 전역 검색(rg 또는 grep)으로 남아있는 참조를 찾아 모두 갱신하십시오.
19-19: tools:listitem 리소스 존재 및 유효성 확인 완료
app/src/main/res/layout/search_horizontal_content_item.xml 파일이 존재하며 XML 파싱이 정상이라 fragment의 tools:listitem 미리보기용으로 사용해도 무방합니다.
Related issue 🛠
Work Description 📝
Screenshot 📸
![]()
필터 화면2025-09-03.00.16.31.mp4
상세화면
2025-09-03.00.22.48-2.mp4
Uncompleted Tasks 😅
To Reviewers 📢
필터에서 날짜 영어로 뜨는데 가상기기여서 그런지 확인한번 해봐야할 거 같습니다.
Summary by CodeRabbit
New Features
UI/UX
Chores
Summary by CodeRabbit
New Features
UI/Style
Chores