Skip to content

Comments

[Feat] search UI#80

Merged
nasohee merged 24 commits intodevelopfrom
feat-search-ui
Sep 23, 2025
Merged

[Feat] search UI#80
nasohee merged 24 commits intodevelopfrom
feat-search-ui

Conversation

@nasohee
Copy link
Collaborator

@nasohee nasohee commented Sep 2, 2025

Related issue 🛠

Work Description 📝

  • 작업 내용

Screenshot 📸

필터 화면
2025-09-03.00.16.31.mp4

상세화면

2025-09-03.00.22.48-2.mp4

Uncompleted Tasks 😅

  • Task1

To Reviewers 📢

필터에서 날짜 영어로 뜨는데 가상기기여서 그런지 확인한번 해봐야할 거 같습니다.

Summary by CodeRabbit

  • New Features

    • 검색 필터 화면 추가: 날짜(시작/종료), 종·품종 다중 선택, 지역(시/군/구) 선택, 초기화·적용 지원
    • 날짜 선택 바텀시트 다이얼로그 도입
    • 상세 화면(보호/목격/실종)에 네이버 지도 연동 및 위치 복사/지도 보기 액션 추가
  • UI/UX

    • 상세 화면 정보 레이아웃 전면 개편: 특이사항, 연락처, 위치 정보 가독성 향상
    • 이미지 뷰페이저 단순화 및 플레이스홀더 적용
    • 검색 탭 상단 배너/정렬 버튼 구성으로 헤더 정리
    • 아이콘·버튼·라디오·칩 스타일 및 드로어블 다수 업데이트
  • Chores

    • Material 3(Android) 라이브러리 종속성 추가

Summary by CodeRabbit

  • New Features

    • 검색 필터 화면 및 날짜 선택 다이얼로그 추가(종/품종·지역·기간·초기화/적용).
    • 검색 목록 헤더 배너·그리드/리스트 전환, 신규 리스트 어댑터 및 품종 필터 UI 추가.
    • 상세 화면들에 지도 표시(주소 지오코딩·마커), 지도 열기 및 위치 복사/통화/메시지 동작 추가.
  • UI/Style

    • 상세 레이아웃 재구성(특이사항, 신고자/연락처, RFID 등) 및 아이콘·배경·디자인 개선.
    • 이미지 슬라이더 및 칩/버튼 스타일 개선, 다양한 drawable/문자열/치수 리소스 추가.
  • Chores

    • Material3(Android) 라이브러리 추가.

@coderabbitai
Copy link

coderabbitai bot commented Sep 2, 2025

Walkthrough

앱에 Material3 의존성 추가, 검색 필터 화면·날짜 다이얼로그·품종/지역 어댑터 및 네비게이션 추가, DTO/도메인 모델 필드 확장·이름 변경, 상세 화면 3종에 지도·이미지 페이저·UI 재구성 적용, 탭 화면들을 더미데이터 기반과 새로운 리스트 어댑터로 리팩터링, 다수 리소스(레이아웃/드로어블/문자열/스타일) 추가·수정.

Changes

Cohort / File(s) Summary
Dependencies
app/build.gradle.kts, gradle/libs.versions.toml
Material3(Android) 라이브러리 선언·버전 추가 및 app 의존성 등록
DTO / Domain 모델
app/src/main/java/.../DetailReportResponseDto.kt, .../domain/model/search/DetailProtectData.kt, .../domain/model/search/DetailReportData.kt
DetailReportResponseDto에 age/rfid/userPhone/surroundPlace 옵션 필드 추가, DetailProtectData.additionalDescription→specialNote/DetailReportData 추가 필드 및 필드명 변경
Mapper
app/src/main/java/.../DetailSearchResponseDtoMapper.kt
도메인 필드명 변경 반영(specialNote) 및 DetailReportData 신규 필드 매핑(널 → "정보 없음" 폴백)
검색 필터 UI(신규 화면/다이얼로그/어댑터)
app/src/main/java/.../SearchFilterFragment.kt, .../dialog/SearchFilterDateDialog.kt, .../adapter/SearchBreedRVAdapter.kt, .../adapter/SearchFilterLocationRVAdapter.kt, .../model/SearchDate.kt, .../model/DummySearchData.kt, app/src/main/res/layout/fragment_search_filter.xml, app/src/main/res/layout/dialog_search_filter_date.xml, app/src/main/res/navigation/nav_graph.xml
SearchFilterFragment/날짜 다이얼로그/품종 어댑터/위치 어댑터/타입 enum/더미 데이터·레이아웃·네비게이션 액션 추가; 어댑터 선택 갱신 API 추가
탭 화면 리팩터링 / 리스트 어댑터 도입
app/src/main/java/.../tablayout/SearchAllFragment.kt, .../SearchReportFragment.kt, .../SearchRescueFragment.kt, app/src/main/java/.../SearchDummyProvider.kt, app/src/main/java/.../adapter/SearchListAdapter.kt, app/src/main/res/layout/fragment_search_{all,report,rescue}.xml, app/src/main/res/layout/item_search_header.xml
ViewModel 흐름 제거 후 더미 데이터 사용, SearchListAdapter(헤더+토글) 도입, RecyclerView id/레이아웃 단순화 및 네비게이션/페이징 로직 재배치
상세 화면: 지도·이미지·UI 재구성
app/src/main/java/.../detail/SearchDisappearDetailFragment.kt, .../SearchProtectingDetailFragment.kt, .../SearchWitnessDetailFragment.kt, app/src/main/java/.../adapter/SearchDetailVPAdapter.kt, app/src/main/res/layout/fragment_search_detail_*.xml, app/src/main/res/layout/item_search_detail_img.xml
Naver MapView 통합(지오코딩/마커/카메라), MapView 생명주기 메서드 추가, 북마크 상태 로직, 이미지 페이저 단순화(Any 지원), 상세 레이아웃·ID 대대적 변경
어댑터/데코레이션 보완
app/src/main/java/.../adapter/SearchFilterLocationRVAdapter.kt, app/src/main/java/.../SearchSpacingItemDecoration.kt, app/src/main/java/.../adapter/SearchBreedRVAdapter.kt, app/src/main/java/.../adapter/SearchDetailVPAdapter.kt
위치 어댑터에 updateSelected 추가, 텍스트 스타일 조정, 스페이싱 데코 첫 아이템/INVALID 포지션 처리, 품종 어댑터 필터/선택 로직 추가, DetailVPAdapter 다형성 처리
리소스: 레이아웃/아이콘/드로어블/스타일/문자열/디멘션
app/src/main/res/layout/*, app/src/main/res/drawable/*, app/src/main/res/values/{strings,styles,dimens}.xml
다수 레이아웃(필터, 상세, 아이템) 추가·변경, 드로어블(아이콘/배경) 추가·수정, 문자열 리소스 대량 추가/수정, DatePicker 스타일·BottomSheet dim 속성 추가, SEARCH_IMAGE_MARGIN 추가
테스트/더미 데이터 제공자
app/src/main/java/.../model/SearchDummyProvider.kt, app/src/main/java/.../model/DummySearchData.kt
DEBUG 전용 더미 데이터 공급자 및 Search 관련 더미 팩토리 추가

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)
Loading
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 업데이트
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

Poem

필터 열고 지도를 띄우니 🌟
품종 칩이 톡, 날짜가 춤추네 📅
더미라도 손끝에 흐름 있어,
북마크는 반짝, 복사는 찰칵—
조회의 하루, 발견의 시작.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed 제목 “[Feat] search UI”는 검색 UI 구현이라는 PR의 주요 변경 사항을 짧고 명확하게 요약하며 가독성과 목적 전달 면에서 적절합니다.
Linked Issues Check ✅ Passed 연결된 이슈 #76의 모든 구현 요구 사항이 PR에 반영되어 배너, 필터 화면, 날짜·축종·품종 선택, 위치 선택, 상세 화면 및 하단 기능이 모두 구현되었습니다.
Out of Scope Changes Check ✅ Passed PR 내 모든 변경 사항은 검색 UI 구현과 직접적으로 연관되어 있으며 제외해야 할 불필요한 변경은 없습니다.
Description Check ✅ Passed 제공된 PR 설명은 템플릿의 필수 섹션을 모두 포함하지만 Work Description 섹션이 구체적인 작업 내용을 담고 있지 않아 해당 부분을 보강하는 것이 좋습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-search-ui

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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_ENDSearchDateType.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"

Copy link
Collaborator

@ikseong00 ikseong00 left a comment

Choose a reason for hiding this comment

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

오랜만에 View 코드 보니 익숙치 않네요
혹시 실행 영상도 올려주실 수 있나요 ??

@nasohee
Copy link
Collaborator Author

nasohee commented Sep 2, 2025

오랜만에 View 코드 보니 익숙치 않네요 혹시 실행 영상도 올려주실 수 있나요 ??

스크롤 범위만 수정하고 바로 올리겠습니다!!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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에서 idlong 타입으로 변경하면 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.withContext

strings.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에서 토글 클릭만 전달하고 실제 아이콘 상태 전환은 안 합니다. 현재 setGridModenotify…로 전체 리바인드하더라도 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7835e2e and b6e0070.

📒 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() ?: -1L
app/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 참조가 코드베이스에서 제거되었습니다.

Comment on lines 39 to 41
private var lastProtectId = Long.MAX_VALUE
private var isNewList = false

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

lastProtectId 업데이트 경로 확인 필요

페이징에 쓰는 lastProtectId가 어디서 갱신되는지 보이지 않습니다. 데이터 수신 시 마지막 아이템 id로 갱신하지 않으면 다음 페이지 요청이 반복되거나 멈출 수 있어요.

아래 스크립트로 레포에서 갱신 지점 존재 여부를 확인해 주세요:

Also applies to: 186-195


🏁 Script executed:

#!/bin/bash
# lastProtectId 갱신/참조 위치 확인
rg -n -C2 'lastProtectId' --type=kt

Length 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.

Comment on lines +217 to 231
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)
}
}
})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

페이징 시 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.

Suggested change
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.

@ikseong00
Copy link
Collaborator

고생하셨습니다!
네이버 지도 관련해서는 제 PR 확인하시면 될 것 같습니다!!

UI 쪽에서 살짝 씩의 수정사항이 있을 것 같은데, QA 때 진행하시죠!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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() ?: cardId
app/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.neutering
app/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.doOnTextChanged
app/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

📥 Commits

Reviewing files that changed from the base of the PR and between b6e0070 and f446f6c.

📒 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 제약 조합이 깔끔합니다. 스크롤 영역/바텀 섹션 겹침 이슈 없겠네요.

Comment on lines +80 to +81
// observeViewModel()
// fetchDetailData()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

데이터 로딩이 비활성화됨 — 주석 해제 필요

상세 데이터가 로드되지 않아 화면이 더미 상태로 남습니다. 병합 전 반드시 활성화해 주세요.

-//        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.

Suggested change
// 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).

Comment on lines +76 to 79
android:layout_marginStart="24dp"
android:layout_marginTop="30dp"
android:text="@string/search_name_welshcorgi"
app:layout_constraintStart_toStartOf="parent"
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment on lines +127 to +133
<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" />
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +114 to +115
<string name="search_detail_shelter_phone_num">전화번호</string>
<string name="search_detail_shelter_phone_num_value">010–2868–2108</string>
Copy link

Choose a reason for hiding this comment

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

💡 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/values

Length 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.

Suggested change
<string name="search_detail_shelter_phone_num">전화번호</string>
<string name="search_detail_shelter_phone_num_value">01028682108</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.

Copy link
Collaborator

@t1nm1ksun t1nm1ksun left a comment

Choose a reason for hiding this comment

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

코드량이 많은데 고생 많으셨습니다!
제가 처음부터 보던 코드가 아니기도하고 xml이라 질문몇개 남겼습니다...ㅠ

private var visibleItems = allItems.toMutableList()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val b = ItemSearchBreedsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 변수가 의미하는게 뭘까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

화면에 보여지는 list입니다. 초기에 모든 항목 표시하도록 초기화 해주고 필터링되면 값이 갱신됩니다

Comment on lines +61 to +62
@SuppressLint("NotifyDataSetChanged")
@Suppress("UNCHECKED_CAST")
Copy link
Collaborator

Choose a reason for hiding this comment

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

이친구들은 어떤 역할인가요??

혹시 ListAdapter + DiffUtil 에 대해 알고계신가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

값 바뀌면 리싸이클러뷰 아이템 다시 그리도록 했는데 성능적으로 비효율인거 같습니다.
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>) :
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any 말고 가독성 측면에서 더 좋은 타입도 있을 것 같아요~
실제 들어오게 되는 클래스나 제네릭...?

Comment on lines +18 to +21
Glide.with(binding.root)
.load(item)
.into(binding.ivSearchDetailContent)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Coil이 아닌 Glide를 사용하신 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

제가 전부터 Glide를 거의 써와서.. 그냥 글라이드 쓴거같습니다.....

private val onFilterClick: () -> Unit,
private val onToggleClick: () -> Unit,
private val onItemClick: (SearchRv) -> Unit,
private val onBookmarkClick: (Long, Boolean, String) -> Unit
Copy link
Collaborator

Choose a reason for hiding this comment

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

Long, Booelan, String 이 무얼 나타내는지 궁금합니다! -> 자주 쓰이는 조합(특정 객체를 나타내는 경우)이라면 하나의 데이터 클래스로 묶는건 어떨까요


private fun fetchDetailData() {
when (tag) {
"목격신고", "실종신고" -> viewModel.getDetailSearchReport(cardId)
Copy link
Collaborator

Choose a reason for hiding this comment

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

하드코딩 수정해주시면 감사하겠습니다~

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between f446f6c and 9589246.

📒 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 미리보기용으로 사용해도 무방합니다.

@nasohee nasohee merged commit cc76d8e into develop Sep 23, 2025
1 check passed
This was referenced Oct 6, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 23, 2025
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 조회 UI 구현

3 participants