Skip to content

Conversation

@casper-jr
Copy link
Member

@casper-jr casper-jr commented Aug 29, 2025

🚀 이슈번호

✏️ 변경사항

  • 등록된 메뉴 검색 화면에서 검색된 결과를 지도에 표시하고, 핀을 활성화 하는 기능 구현
  • 뒤로가기 로직에서의 상태 처리 수정
  • 지도앱에서 선택된 핀의 가게 정보를 검색한 결과로 이동하도록 버튼 로직 구현

📷 스크린샷

Screen_Recording_20250829_130624_OurMenu.mp4

✍️ 사용법

🎸 기타

Summary by CodeRabbit

  • 신규 기능

    • 지도 검색 결과에서 브라우저 웹 검색으로 이동 기능 추가
  • 개선

    • 검색 결과 선택 시 바텀시트 자동 표시 및 표시 높이/조건 개선
    • 뒤로가기 동작 통합: 검색 배경/바텀시트 상태에 따른 적절한 종료·숨김 처리
    • 검색 히스토리 클릭 시 포커스 해제 후 바텀시트 표시
    • 빈 검색어 입력 시 내 메뉴 전환 및 검색 UI 정리
    • 지도 마커 표시·카메라 이동 로직 정교화
    • 메뉴 이미지의 가로 정렬, 간격 및 크기 조정으로 시각적 균형 향상
  • 기타

    • 검색/히스토리 응답에 mapId 필드 추가
    • 진단용 로그 보강

@casper-jr casper-jr self-assigned this Aug 29, 2025
@casper-jr casper-jr added ✨ feat Good for newcomers ⚡️ fix Extra attention is needed 🦦재우 👌 OK Merge labels Aug 29, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 29, 2025

Walkthrough

지도 검색 기능 흐름을 확장: 데이터 모델에 mapId 추가, ViewModel에 activeMapId/웹검색 쿼리/클리어 로직 도입, 검색 결과를 기반으로 마커/카메라 포커싱 갱신, 화면에서 바텀시트 표시/뒤로가기 처리/웹 검색 인텐트 연동, 일부 UI 레이아웃 조정 및 프리뷰 업데이트.

Changes

Cohort / File(s) Summary
Map 검색 응답 모델 확장
app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchHistoryResponse.kt, app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt
응답 모델에 mapId: Long 추가 (MapSearchResponse에는 mapId, menuId 추가). 각 프로퍼티에 @SerialName 매핑 추가 및 생성자 순서 변경(History는 mapId를 첫 인자로 삽입). JSON 직렬화/역직렬화 스키마 변경.
검색 화면 동작 업데이트
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt
ViewModel의 activeMapId 사용으로 바텀시트 자동 표시/숨김 제어, BackHandler 확장(검색 배경/바텀시트 처리), 히스토리 클릭 시 포커스 정리 후 시트 표시, 웹 검색 인텐트(ACTION_VIEW) 연동, 로그/임포트 추가.
검색 ViewModel 로직 확장
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
검색 성공 후 전체 메뉴 조회→검색결과 mapId로 필터→마커/카메라 갱신→첫 결과 활성화 및 상세 조회. getMapMenuDetail 흐름 보강. clearActiveMapId()suspend fun getWebSearchQuery(mapId: Long) 추가. 로깅 조정.
검색 히스토리 프리뷰 정합성
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchHistory.kt
프리뷰 샘플 데이터에 mapId 추가 및 생성자 인자 순서 조정.
바텀시트 이미지 레이아웃 조정
app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt
이미지 Row 간격을 spacedBy(4.dp)로 변경, 각 이미지 weight(1f) + height(80.dp)로 균등 배치, 이미지 요청 크기 소폭 조정, Spacer 제거(레이아웃/스타일 변경).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Screen as SearchMenuScreen
  participant VM as SearchMenuViewModel
  participant Repo as DataSource
  participant Map as MapView/Markers

  User->>Screen: 검색어 입력 및 검색 실행
  Screen->>VM: search(searchText)
  VM->>Repo: mapSearch(query, center)
  Repo-->>VM: MapSearchResponse(list with mapId/menuId)
  VM->>Repo: getMyMenus()
  Repo-->>VM: MyMenus(all)
  VM->>VM: filter myMenus by mapId from results
  VM->>Map: render markers(filtered)
  VM->>VM: set activeMapId = firstResult.mapId
  VM->>Repo: getMapDetail(activeMapId)
  Repo-->>VM: MapDetail
  VM-->>Screen: state(updated results, activeMapId)
  Screen->>Screen: show BottomSheet when activeMapId != null
Loading
sequenceDiagram
  autonumber
  actor User
  participant Screen as SearchMenuScreen
  participant VM as SearchMenuViewModel
  participant OS as Android OS Intent

  User->>Screen: 히스토리 아이템 클릭
  Screen->>Screen: 포커스 해제, 배경 숨김
  Screen->>VM: getMapMenuDetail(menuId or mapId)
  VM-->>Screen: state(activeMapId set)
  Screen->>Screen: BottomSheet 표시

  User->>Screen: "지도에서 보기" 클릭
  Screen->>VM: getWebSearchQuery(activeMapId)
  VM-->>Screen: naver search URL or ""
  alt URL 존재
    Screen->>OS: Intent.ACTION_VIEW(URL)
  else URL 없음
    Screen-->>User: 로그/무작동
  end

  User->>Screen: 뒤로가기
  alt 검색 배경 보임
    Screen->>Screen: 배경 숨김, 포커스 해제
  else 바텀시트 보임
    Screen->>VM: clearActiveMapId()
    VM->>Map: refresh markers (clear selection)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
크롤링 api 연결 (#59) 외부 크롤링 API의 실제 연결/설정 변경은 diff에서 확인되지 않음.
검색에서 사용할 ViewModel 사용환경 구성 (#59)
검색 결과에 해당하는 위치에 핀 추가 (#59)
검색 결과 조회/ 지도 포커싱 (#59)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
바텀시트 이미지 레이아웃/사이즈 변경 (app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt) 스타일/레이아웃 변경으로 검색 기능 구현 목표와 직접적 관련 없음.
외부 브라우저로의 웹 검색 인텐트 추가 (app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt) 이슈 #59의 요구 목록에 외부 웹 검색 이동은 명시되지 않음.
getWebSearchQuery(mapId) 유틸 추가 (app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt) 외부 검색 URL 생성 기능은 이슈의 핵심 목표(지도 내 검색/핀/포커싱)를 벗어남.

Possibly related PRs

Suggested reviewers

  • ikseong00

Poem

핀 하나, mapId의 불빛 아래
카메라는 살짝 줌, 시트는 살며시 올라오네
클릭 한 번에 길이 열리고, 메뉴가 반짝
로그는 속삭이고, Intent는 문을 두드려
지도 위에 작은 축제가 벌어졌네 ✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

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/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (2)

143-169: 이미지 스트림 누수 가능성 및 불필요한 2배 업스케일

InputStream을 닫지 않고, 원본을 2배로 키워 메모리 사용량이 커집니다. 최소한 스트림 닫기/연결 해제와 다운스케일 기준 적용이 필요합니다.

- val connection = URL(url).openConnection()
+ val connection = (URL(url).openConnection() as java.net.HttpURLConnection)
  connection.connectTimeout = 5000
  connection.readTimeout = 5000
  connection.connect()
-
- val input = connection.getInputStream()
- // 원본 비트맵 로드
- val originalBitmap = BitmapFactory.decodeStream(input)
- 
- // 원하는 크기로 리사이징 (예: 원본의 2배)
- originalBitmap?.let { bitmap ->
-     val width = bitmap.width * 2  // 원본 너비의 2배
-     val height = bitmap.height * 2  // 원본 높이의 2배
-     bitmap.scale(width, height).also {
-         // 원본 비트맵 메모리 해제
-         if (it != bitmap) {
-             bitmap.recycle()
-         }
-     }
- }
+ val originalBitmap = connection.inputStream.use { BitmapFactory.decodeStream(it) }
+ // 필요할 때에만 다운스케일 (예: 최대 96px)
+ originalBitmap?.let { bmp ->
+     val max = 96
+     val scale = maxOf(bmp.width.toFloat() / max, bmp.height.toFloat() / max)
+     if (scale > 1f) {
+         val w = (bmp.width / scale).toInt()
+         val h = (bmp.height / scale).toInt()
+         bmp.scale(w, h)
+     } else bmp
+ }.also {
+     // HttpURLConnection 해제
+     connection.disconnect()
+ }
  • 추후 권장: Coil/Glide로 캐싱+사이즈 지정 후 Bitmap 획득. 마커 재그리기 시 네트워크 재요청을 피하려면 LruCache<url, Bitmap> 도입 권장.

204-221: 라벨 클릭 리스너 중복 등록 + 좌표 동등성 비교 취약

addMarker 호출마다 setOnLabelClickListener가 재등록됩니다. 또한 실수 비교에 == 사용은 위험합니다.

- map.labelManager?.layer?.addLabel(options)
- map.setOnLabelClickListener { kakaoMap, labelLayer, label ->
+ val label = map.labelManager?.layer?.addLabel(options)
+ // TODO: 초기화 시 1회만 등록(예: initializeMap에서)
+ map.setOnLabelClickListener { kakaoMap, labelLayer, label ->
     ...
-     _myMenus.value?.find { menu ->
-         menu.mapY == label.position.latitude && menu.mapX == label.position.longitude
-     }?.let { matchingMenu ->
+     _myMenus.value?.find { menu ->
+         kotlin.math.abs(menu.mapY - label.position.latitude) < 1e-6 &&
+         kotlin.math.abs(menu.mapX - label.position.longitude) < 1e-6
+     }?.let { matchingMenu ->
         ...
     }
  • 더 좋음: 라벨 추가 시 mapId를 태그/맵에 저장하고 클릭 시 O(1)로 역참조(좌표 비교 제거).
🧹 Nitpick comments (6)
app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt (1)

157-159: 이미지 비율 안정화를 위해 contentScale = ContentScale.Crop 권장

고정 높이(80.dp) + 가변 너비(가중치)라 원본 비율에 따라 레터박스/왜곡이 생길 수 있어요. Crop으로 일관되게 채우도록 제안합니다.

-                contentDescription = null,
+                contentDescription = null,
+                contentScale = ContentScale.Crop,
                 modifier = Modifier
                     .weight(1f)
                     .height(80.dp)
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt (1)

90-93: 바텀시트 표시 트리거 로직 OK, 다만 중복 트리거 방지 여지

LaunchedEffect(menusOnPin, activeMapId)에서 이미 열려있을 때 또 true로 세팅하는 반복이 발생할 수 있어요. 미세한 깜빡임 방지를 위해 상태 변경 최소화 권장.

-    if (activeMapId != null && menusOnPin != null && menusOnPin?.isNotEmpty() == true) {
-        showBottomSheet = true
-    }
+    if (!showBottomSheet &&
+        activeMapId != null &&
+        menusOnPin?.isNotEmpty() == true
+    ) {
+        showBottomSheet = true
+    }
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (4)

75-102: Log 태그 명 불일치("AddMenuViewModel") → "SearchMenuViewModel"로 통일

로그 필터링/검색에 불편합니다. 파일 전반에서 태그를 통일해 주세요.

- Log.d("AddMenuViewModel", "location permission : $granted")
+ Log.d("SearchMenuViewModel", "location permission : $granted")
...
- Log.d("AddMenuViewModel", "initialize Map")
+ Log.d("SearchMenuViewModel", "initialize Map")
...
- Log.d("AddMenuViewModel", "location success: lat=${it.latitude}, long=${it.longitude}")
+ Log.d("SearchMenuViewModel", "location success: lat=${it.latitude}, long=${it.longitude}")
...
- Log.d("AddMenuViewModel", "location fail")
+ Log.d("SearchMenuViewModel", "location fail")

382-386: 카메라 포커싱은 활성 항목 우선

첫 항목 고정보다 activeMapId가 있으면 그 좌표로 이동하는 게 자연스럽습니다.

- // 첫 번째 검색 결과로 카메라 이동 TODO: 현재 위치랑 가까운 결과로 이동
- moveCamera(menus[0].mapY, menus[0].mapX)
+ // 활성 핀으로 포커싱(없으면 첫 항목)
+ val focus = menus.find { it.mapId == _activeMapId.value } ?: menus.first()
+ moveCamera(focus.mapY, focus.mapX)

391-394: clearActiveMapId 시 바텀시트/상세 상태도 초기화

UI 잔상 방지를 위해 메뉴 상세 상태를 함께 비워주세요.

 fun clearActiveMapId() {
     _activeMapId.value = null
+    _menusOnPin.value = emptyList()
     refreshMarkers()
 }

227-233: 마커 재그리기 시 네트워크/디코드 반복 비용 큼(스파이크 가능)

활성/비활성 아이콘을 URL→Bitmap으로 매번 로드합니다. 이미지 캐시 도입 권장.

  • 제안: ViewModel 내 LruCache<String, Bitmap> 캐시를 두고, addMarker에서 캐시 히트 시 즉시 사용. 미스 시 로드 후 put.
  • 활성 아이콘 토글만으로 재다운로드를 피하려면 활성/비활성 각각의 URL 키로 별도 캐시.

Also applies to: 370-387

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 517219b and 82fac6b.

📒 Files selected for processing (6)
  • app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchHistoryResponse.kt (1 hunks)
  • app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt (1 hunks)
  • app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt (1 hunks)
  • app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchHistory.kt (1 hunks)
  • app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt (8 hunks)
  • app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt (1)
app/src/main/java/com/kuit/ourmenu/ui/common/GoToMapButton.kt (1)
  • GoToMapButton (39-76)
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (3)
app/src/main/java/com/kuit/ourmenu/ui/addmenu/viewmodel/AddMenuViewModel.kt (4)
  • showSearchResultOnMap (247-260)
  • clearMarkers (171-175)
  • addMarker (147-168)
  • moveCamera (114-121)
app/src/main/java/com/kuit/ourmenu/data/service/MapService.kt (2)
  • getMapDetail (16-57)
  • getMapDetail (18-21)
app/src/main/java/com/kuit/ourmenu/data/repository/MapRepository.kt (1)
  • getMapDetail (12-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build
🔇 Additional comments (3)
app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt (1)

139-142: Row 간격 변경 깔끔합니다

Arrangement.spacedBy(4.dp)로 간격 관리하는 방향 좋아요. 내부 Spacer 제거와 조합이 잘 맞습니다.

app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchHistoryResponse.kt (1)

8-10: mapId 기본값 지정 여부 검토
구버전 API 응답에 mapId가 누락될 경우 역직렬화 실패(크래시)가 발생합니다. 백엔드에서 항상 내려오는지 확인하고, 보장이 없다면 기본값(val mapId: Long = -1L 등)을 지정하세요.

app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchHistory.kt (1)

165-180: 프리뷰 데이터가 모델 변경(mapId 추가)과 정합합니다

샘플 데이터 정렬/필드 추가 모두 OK.

Comment on lines +8 to +11
@SerialName("mapId")
val mapId: Long,
@SerialName("menuId")
val menuId: Long,
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

응답 스키마 확장(mapId, menuId) 시 기본값 없으면 호환성 이슈

구버전 서버/캐시와 통신 시 역직렬화 실패 위험. 기본값으로 방어해두는 걸 추천합니다.

 data class MapSearchResponse(
     @SerialName("mapId")
-    val mapId: Long,
+    val mapId: Long = -1L,
     @SerialName("menuId")
-    val menuId: Long,
+    val menuId: Long = -1L,

🏁 Script executed:

#!/bin/bash
set -eu

echo "Inspecting MapSearchResponse.kt for serialization annotations and imports"
sed -n '1,20p' app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt

echo
echo "Finding all references to MapSearchResponse in the codebase"
rg -n --context 2 'MapSearchResponse' -g '*.kt' app/src

Length of output: 3803


mapId와 menuId에 기본값(-1L) 추가
역직렬화 시 필드 누락 시 MissingFieldException을 방어하려면 기본값 설정이 필요합니다 (kotlinlang.org, stackoverflow.com).

 data class MapSearchResponse(
     @SerialName("mapId")
-    val mapId: Long,
+    val mapId: Long = -1L,
     @SerialName("menuId")
-    val menuId: Long,
+    val menuId: Long = -1L,
📝 Committable suggestion

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

Suggested change
@SerialName("mapId")
val mapId: Long,
@SerialName("menuId")
val menuId: Long,
@SerialName("mapId")
val mapId: Long = -1L,
@SerialName("menuId")
val menuId: Long = -1L,
🤖 Prompt for AI Agents
In
app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt
around lines 8 to 11, the properties mapId and menuId currently have no defaults
and can trigger MissingFieldException during deserialization if those fields are
absent; add default values of -1L to both properties (e.g., declare them with =
-1L) so they become optional for kotlinx.serialization and deserialization will
not throw when fields are missing.

model = ImageRequest.Builder(LocalPlatformContext.current)
.data(imgUrls[i])
.size(104, 80)
.size(108, 80)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Coil .size(108, 80)는 픽셀 하드코딩이라 해상도별 블러/언더페치 위험

Compose는 이미지의 실제 레이아웃 사이즈를 전달해주므로 명시적 픽셀 사이즈는 불필요하고, 밀도에 따라 품질 이슈가 납니다. 제거 권장.

-                            .size(108, 80)
📝 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
.size(108, 80)
🤖 Prompt for AI Agents
In
app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt
around line 149, remove the hardcoded Coil .size(108, 80) call (pixel values)
from the image request; instead rely on Compose to provide the image's actual
layout size (or use Coil's density-aware/ORIGINAL size options if explicit
sizing is required). Update the code to omit the .size(...) invocation so the
image scales correctly across screen densities and prevent blur/underfetch
issues.

Comment on lines +174 to 177
sheetPeekHeight = if (showBottomSheet) {
val itemCount = menusOnPin?.size ?: 0
(singleItemHeight * itemCount) + dragHandleHeight
} else 0.dp,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

peek 높이가 화면을 초과할 수 있음 — 화면 높이 기준 상한선 필요

아이템 3개만으로도 300.dp * 3이 900dp라 기기 따라 시트를 꽉 채우거나 초과할 수 있어요. 화면의 80~90%로 cap 하세요.

-        sheetPeekHeight = if (showBottomSheet) {
-            val itemCount = menusOnPin?.size ?: 0
-            (singleItemHeight * itemCount) + dragHandleHeight
-        } else 0.dp,
+        sheetPeekHeight = if (showBottomSheet) {
+            val itemCount = menusOnPin?.size ?: 0
+            val desired = (singleItemHeight * itemCount) + dragHandleHeight
+            val maxPeek = with(LocalConfiguration.current) { (screenHeightDp.dp * 0.9f) }
+            desired.coerceAtMost(maxPeek)
+        } else 0.dp,

추가 import 필요:

import androidx.compose.ui.platform.LocalConfiguration
🤖 Prompt for AI Agents
In app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt
around lines 174 to 177, the computed sheetPeekHeight can exceed the device
screen (e.g., 300.dp * 3 = 900.dp); clamp the computed peek height to a
percentage of the screen (80–90%) by reading screen height via
LocalConfiguration (import androidx.compose.ui.platform.LocalConfiguration),
convert configuration.screenHeightDp to Dp, compute a maxPeek =
screenHeightDp.dp * 0.9f (or 0.8f), and set sheetPeekHeight =
min(calculatedPeek, maxPeek) so the bottom sheet never exceeds the screen cap.

Comment on lines 226 to +246
// 검색 시 현재 지도 중심 좌표 사용
if (searchText.isNotEmpty()) {
// 검색 직전에 현재 지도 중심 좌표 업데이트
viewModel.updateCurrentCenter()

val center = viewModel.getCurrentCoordinates()
if (center != null) {
val (latitude, longitude) = center
Log.d("SearchMenuScreen", "검색 위치: $latitude, $longitude")

// 검색어와 현재 좌표로 스토어 정보 요청
viewModel.getMapSearchResult(
query = searchText,
long = longitude,
lat = latitude
)

showBottomSheet = true
showSearchBackground = false
}
}else{
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

현재 중심 좌표 갱신/조회 사이의 경쟁 조건 가능성

updateCurrentCenter() 호출 직후 getCurrentCoordinates()를 즉시 읽으면 비동기 갱신이 아직 반영되지 않았을 수 있어요. 한 함수에서 좌표를 리턴하도록 합치거나, 콜백/Flow 완료 시점에 이어서 검색 호출하는 방식 권장.

다음 스크립트로 ViewModel 구현을 확인해 동기/비동기 동작을 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# updateCurrentCenter / getCurrentCoordinates 정의 및 사용 맥락 확인
rg -nP -C3 'fun\s+updateCurrentCenter\s*\(|fun\s+getCurrentCoordinates\s*\(' --type=kt

Length of output: 115


타입 필터 문제로 기존 스크립트가 실패했습니다. 전체 .kt 파일에서 함수 정의를 검색해 주세요.


🏁 Script executed:

#!/bin/bash
# updateCurrentCenter 및 getCurrentCoordinates 정의 검색
rg -n -C3 'fun updateCurrentCenter' -g '*.kt'
rg -n -C3 'fun getCurrentCoordinates' -g '*.kt'

Length of output: 3261


updateCurrentCenter() 비동기 호출로 인한 레이스 컨디션 위험

updateCurrentCenter()가 viewModelScope.launch 내부에서 currentCenter를 업데이트하므로 즉시 getCurrentCoordinates()를 호출하면 이전 좌표가 반환될 수 있습니다. updateCurrentCenter()를 suspend 함수로 변경해 반환값으로 center를 전달하거나, 좌표 업데이트 완료 시점에 이어서 getMapSearchResult()를 호출하도록 뷰모델 단에서 통합 처리하세요.

🤖 Prompt for AI Agents
In app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt
around lines 226 to 246, there's a race where updateCurrentCenter() is
asynchronous so calling getCurrentCoordinates() immediately can return stale
coordinates; change updateCurrentCenter() to a suspend function (or provide a
suspend/returning API that yields the updated center) or expose the updated
center via a Flow/LiveData and move the getMapSearchResult(...) call into the
ViewModel after the center is actually updated so the UI only requests search
results when the ViewModel confirms the new coordinates (i.e., update the
ViewModel to perform updateCurrentCenter and then call getMapSearchResult
internally or return the new center to the caller before invoking
getMapSearchResult).

Comment on lines +259 to 273
onClick = {
// 네이버 지도에 해당 가게 검색 결과로 이동
if (activeMapId == null) {
Log.d("SearchMenuScreen", "활성화된 Map ID가 없습니다.")
} else {
scope.launch {
val searchQuery = viewModel.getWebSearchQuery(activeMapId!!)
if (searchQuery.isNotBlank()) {
Log.d("SearchMenuScreen", "intent query: $searchQuery")
val webIntent = Intent(Intent.ACTION_VIEW, searchQuery.toUri())
context.startActivity(webIntent)
}
}
}
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

웹 이동 인텐트 가드 부족 — 스킴 검증 및 예외 처리 추가

빈 문자열 체크만으로는 부족해요. http/https 스킴 확인과 startActivity 실패 대비가 필요합니다.

-                onClick = {
+                onClick = {
                     // 네이버 지도에 해당 가게 검색 결과로 이동
                     if (activeMapId == null) {
                         Log.d("SearchMenuScreen", "활성화된 Map ID가 없습니다.")
                     } else {
                         scope.launch {
-                            val searchQuery = viewModel.getWebSearchQuery(activeMapId!!)
-                            if (searchQuery.isNotBlank()) {
-                                Log.d("SearchMenuScreen", "intent query: $searchQuery")
-                                val webIntent = Intent(Intent.ACTION_VIEW, searchQuery.toUri())
-                                context.startActivity(webIntent)
-                            }
+                            val searchQuery = viewModel.getWebSearchQuery(activeMapId!!)
+                            val uri = searchQuery.toUri()
+                            if (searchQuery.isNotBlank() && (uri.scheme == "http" || uri.scheme == "https")) {
+                                Log.d("SearchMenuScreen", "intent query: $searchQuery")
+                                runCatching {
+                                    context.startActivity(Intent(Intent.ACTION_VIEW, uri))
+                                }.onFailure {
+                                    Log.w("SearchMenuScreen", "웹 인텐트 실행 실패", it)
+                                }
+                            } else {
+                                Log.w("SearchMenuScreen", "잘못된 URI 스킴 또는 빈 쿼리: $searchQuery")
+                            }
                         }
                     }
                 },
📝 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
onClick = {
// 네이버 지도에 해당 가게 검색 결과로 이동
if (activeMapId == null) {
Log.d("SearchMenuScreen", "활성화된 Map ID가 없습니다.")
} else {
scope.launch {
val searchQuery = viewModel.getWebSearchQuery(activeMapId!!)
if (searchQuery.isNotBlank()) {
Log.d("SearchMenuScreen", "intent query: $searchQuery")
val webIntent = Intent(Intent.ACTION_VIEW, searchQuery.toUri())
context.startActivity(webIntent)
}
}
}
},
onClick = {
// 네이버 지도에 해당 가게 검색 결과로 이동
if (activeMapId == null) {
Log.d("SearchMenuScreen", "활성화된 Map ID가 없습니다.")
} else {
scope.launch {
val searchQuery = viewModel.getWebSearchQuery(activeMapId!!)
val uri = searchQuery.toUri()
if (searchQuery.isNotBlank() && (uri.scheme == "http" || uri.scheme == "https")) {
Log.d("SearchMenuScreen", "intent query: $searchQuery")
runCatching {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}.onFailure {
Log.w("SearchMenuScreen", "웹 인텐트 실행 실패", it)
}
} else {
Log.w("SearchMenuScreen", "잘못된 URI 스킴 또는 빈 쿼리: $searchQuery")
}
}
}
},

Comment on lines 269 to +274
response.onSuccess { result ->
if (result != null && result.isNotEmpty()) {
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: ${result.size}")
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: $result")
// 검색 결과 저장
_searchResult.value = result

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

검색 결과 전체 객체 로그 출력 지양(개인정보/노이즈)

결과 개수만 로그하세요.

- Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: $result")
+ Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: ${result.size}건")
📝 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
response.onSuccess { result ->
if (result != null && result.isNotEmpty()) {
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: ${result.size}")
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: $result")
// 검색 결과 저장
_searchResult.value = result
response.onSuccess { result ->
if (result != null && result.isNotEmpty()) {
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: ${result.size}")
// 검색 결과 저장
_searchResult.value = result
🤖 Prompt for AI Agents
In
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
around lines 269 to 274, the code currently logs the entire search result object
which may contain sensitive data and noisy output; change the logging to only
output the result size (e.g., result.size or result.count()) and a brief
descriptive message, then assign _searchResult.value as before; remove or redact
any direct printing of result contents to avoid leaking personal data.

Comment on lines 275 to 293
// 전체 메뉴 목록을 다시 가져온 후 필터링
val allMenusResponse = mapRepository.getMap()
allMenusResponse.onSuccess { allMenus ->
if (allMenus != null) {
// 전체 메뉴 중에서 검색 결과와 일치하는 것들만 필터링
_myMenus.value = allMenus.filter { menu ->
result.any { searchResult -> searchResult.mapId == menu.mapId }
}

// 검색 결과의 첫 번째 항목을 활성화 상태로 설정
_activeMapId.value = result.firstOrNull()?.mapId
showSearchResultOnMap()

// 첫 번째 검색 결과의 상세 정보를 가져와서 바텀시트에 표시
_activeMapId.value?.let { mapId ->
getMapDetail(mapId)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

전체 메뉴 재조회 과도 + 실패 시 무반응

이미 캐시된 _myMenus가 있으면 재호출을 피하고, 네트워크 실패 시 graceful fallback이 필요합니다.

- // 전체 메뉴 목록을 다시 가져온 후 필터링
- val allMenusResponse = mapRepository.getMap()
- allMenusResponse.onSuccess { allMenus ->
-     if (allMenus != null) {
-         // 전체 메뉴 중에서 검색 결과와 일치하는 것들만 필터링
-         _myMenus.value = allMenus.filter { menu ->
-             result.any { searchResult -> searchResult.mapId == menu.mapId }
-         }
-         
-         // 검색 결과의 첫 번째 항목을 활성화 상태로 설정
-         _activeMapId.value = result.firstOrNull()?.mapId
-         showSearchResultOnMap()
-         
-         // 첫 번째 검색 결과의 상세 정보를 가져와서 바텀시트에 표시
-         _activeMapId.value?.let { mapId ->
-             getMapDetail(mapId)
-         }
-     }
- }
+ // 캐시 우선, 없으면 1회 조회
+ val allMenus = _myMenus.value?.takeIf { it.isNotEmpty() }
+     ?: mapRepository.getMap().getOrElse {
+         Log.w("SearchMenuViewModel", "전체 메뉴 조회 실패(캐시 사용 불가): ${it.message}")
+         emptyList()
+     } ?: emptyList()
+
+ if (allMenus.isEmpty()) {
+     Log.d("SearchMenuViewModel", "표시할 메뉴가 없습니다.")
+     return@launch
+ }
+
+ _myMenus.value = allMenus.filter { menu ->
+     result.any { searchResult -> searchResult.mapId == menu.mapId }
+ }
+ _activeMapId.value = result.firstOrNull()?.mapId
+ showSearchResultOnMap()
+ _activeMapId.value?.let { getMapDetail(it) }

추가로, 이 코루틴은 메인 디스패처입니다. 레포가 IO 디스패처 보장하지 않으면 launch(Dispatchers.IO) 또는 withContext(IO)로 감싸주세요.

📝 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
// 전체 메뉴 목록을 다시 가져온 후 필터링
val allMenusResponse = mapRepository.getMap()
allMenusResponse.onSuccess { allMenus ->
if (allMenus != null) {
// 전체 메뉴 중에서 검색 결과와 일치하는 것들만 필터링
_myMenus.value = allMenus.filter { menu ->
result.any { searchResult -> searchResult.mapId == menu.mapId }
}
// 검색 결과의 첫 번째 항목을 활성화 상태로 설정
_activeMapId.value = result.firstOrNull()?.mapId
showSearchResultOnMap()
// 첫 번째 검색 결과의 상세 정보를 가져와서 바텀시트에 표시
_activeMapId.value?.let { mapId ->
getMapDetail(mapId)
}
}
}
// 캐시 우선, 없으면 1회 조회
val allMenus = _myMenus.value
?.takeIf { it.isNotEmpty() }
?: mapRepository.getMap().getOrElse {
Log.w("SearchMenuViewModel", "전체 메뉴 조회 실패(캐시 사용 불가): ${it.message}")
emptyList()
} ?: emptyList()
if (allMenus.isEmpty()) {
Log.d("SearchMenuViewModel", "표시할 메뉴가 없습니다.")
return@launch
}
_myMenus.value = allMenus.filter { menu ->
result.any { searchResult -> searchResult.mapId == menu.mapId }
}
_activeMapId.value = result.firstOrNull()?.mapId
showSearchResultOnMap()
_activeMapId.value?.let { getMapDetail(it) }
🤖 Prompt for AI Agents
In
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
around lines 275 to 293, avoid unnecessary full list refetch and handle network
failures and threading: first check if _myMenus.value is non-empty and skip
calling mapRepository.getMap() when cached data can satisfy the filter; if you
must call the repository, perform it on IO (wrap the call in
withContext(Dispatchers.IO) or launch(Dispatchers.IO)) and surround it with
try/catch so failures fall back to the existing _myMenus (or surface a
user-facing error) instead of leaving the UI unresponsive; after a successful
fetch, filter as before and set _activeMapId/getMapDetail, but on failure use
the cached list to compute the filtered results or set an error state so the UI
can show a graceful message.

Comment on lines 338 to 366
// 먼저 전체 메뉴를 가져옴
val myMenusResponse = mapRepository.getMap()
myMenusResponse.onSuccess { menus ->
if (menus != null){
val allMenus = menus
val menuDetailResponse = mapRepository.getMapMenuDetail(menuId)
menuDetailResponse.onSuccess { menuDetail ->
Log.d("SearchMenuViewModel", "메뉴 상세 조회 성공: $menuDetail")
// 검색 기록에서 해당 menuId를 가진 항목 찾기
searchHistory.value?.find { it.menuId == menuId }?.let { historyItem ->
Log.d("SearchMenuViewModel", "검색 기록에서 찾은 mapId: ${historyItem.mapId}")
// 가져온 전체 메뉴에서 필터링
_myMenus.value = allMenus.filter { menu ->
menu.mapId == historyItem.mapId
}
// 해당 mapId를 활성화 상태로 설정
_activeMapId.value = historyItem.mapId
// 지도에 검색 결과 표시
showSearchResultOnMap()
// 메뉴 상세 정보를 바텀시트에 표시하기 위해 설정
getMapDetail(historyItem.mapId)
}
}.onFailure {
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
}
}
}.onFailure {
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
Log.d("SearchMenuViewModel", "내 메뉴 조회 실패: ${it.message}")
}
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

getMapMenuDetail 호출 결과를 사용하지 않음(불필요 네트워크 왕복)

현재 menuDetailResponse를 로깅만 하고 버립니다. 제거하거나 히스토리 미존재 시 fallback에 활용하세요.

- val menuDetailResponse = mapRepository.getMapMenuDetail(menuId)
- menuDetailResponse.onSuccess { menuDetail ->
-     Log.d("SearchMenuViewModel", "메뉴 상세 조회 성공: $menuDetail")
-     // 검색 기록에서 해당 menuId를 가진 항목 찾기
-     searchHistory.value?.find { it.menuId == menuId }?.let { historyItem ->
-         ...
-     }
- }.onFailure {
-     Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
- }
+ val historyItem = searchHistory.value?.find { it.menuId == menuId }
+ if (historyItem != null) {
+     _myMenus.value = allMenus.filter { it.mapId == historyItem.mapId }
+     _activeMapId.value = historyItem.mapId
+     showSearchResultOnMap()
+     getMapDetail(historyItem.mapId)
+ } else {
+     Log.d("SearchMenuViewModel", "검색 기록에 menuId=$menuId 없음")
+ }
  • 확인 요청: MapMenuDetailResponse에 mapId가 존재한다면, 히스토리 없을 때 menuDetail.mapId로 활성화하는 분기를 추가해 주세요.

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the MapRepository to inspect getMapMenuDetail signature
rg -nP "interface MapRepository" -n .

# Find the implementation to see return type
rg -nP "class .*MapRepositoryImpl" -n .

# Locate the MapMenuDetailResponse data class
rg -nP "data class MapMenuDetailResponse" -n .

Length of output: 59


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# getMapMenuDetail 선언 위치 및 반환 타입 확인
rg -n --iglob '*.kt' "fun getMapMenuDetail" -C3

# MapMenuDetailResponse 데이터 클래스 및 필드 확인
rg -n --iglob '*.kt' "data class MapMenuDetailResponse" -C3

# 혹시 명칭이 다르면 MapMenuDetail 키워드로도 탐색
rg -n --iglob '*.kt' "MapMenuDetail" -C3

Length of output: 9379


위 응답에서 MapMenuDetailResponsemapId가 없는 것처럼 보입니다. 클래스 정의를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapMenuDetailResponse.kt

Length of output: 914


getMapMenuDetail 결과 활용 및 히스토리 미존재 시 fallback 분기 추가
menuDetailResponse에서 mapId를 꺼내, searchHistory에 해당 메뉴가 없을 때에도 그 mapId_activeMapId 설정·_myMenus 필터링·showSearchResultOnMap()·getMapDetail() 호출 로직을 구현하세요.

🤖 Prompt for AI Agents
In
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
around lines 338 to 366, the current logic only updates _activeMapId, _myMenus,
showSearchResultOnMap(), and getMapDetail() when a matching entry exists in
searchHistory; update the onSuccess block for menuDetailResponse to extract the
mapId from menuDetail (guarding for null), then attempt to find a historyItem by
that menuId and if found behave as before, otherwise use menuDetail.mapId as a
fallback: set _activeMapId to that mapId, set _myMenus to allMenus filtered by
that mapId, call showSearchResultOnMap(), and call getMapDetail(mapId); ensure
you still log success/failure and avoid NPEs by checking menuDetail/mapId
non-null before using them.

Comment on lines +397 to +413
suspend fun getWebSearchQuery(mapId: Long): String {
val baseUrl = "https://map.naver.com/p/search/"
val response = mapRepository.getMapDetail(mapId)
return response.fold(
onSuccess = { menuList ->
if (menuList.isNullOrEmpty()) {
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: 메뉴가 없습니다.")
""
} else {
baseUrl + menuList.first().storeTitle
}
},
onFailure = {
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
""
}
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

웹 검색 쿼리 URL 인코딩 누락 및 불필요 재호출 가능성

공백/특수문자 포함 상호명은 인코딩 필요. 이미 _menusOnPin에 데이터가 있으면 재요청을 피하세요.

- val baseUrl = "https://map.naver.com/p/search/"
- val response = mapRepository.getMapDetail(mapId)
- return response.fold(
+ val baseUrl = "https://map.naver.com/p/search/"
+ val response = mapRepository.getMapDetail(mapId)
+ return response.fold(
     onSuccess = { menuList ->
         if (menuList.isNullOrEmpty()) {
             Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: 메뉴가 없습니다.")
             ""
         } else {
-            baseUrl + menuList.first().storeTitle
+            val encoded = java.net.URLEncoder.encode(
+                menuList.first().storeTitle,
+                java.nio.charset.StandardCharsets.UTF_8.name()
+            )
+            baseUrl + encoded
         }
     },
     onFailure = { 
         Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
         ""
     }
 )
  • 선택: _menusOnPin.value?.firstOrNull()?.storeTitle 우선 사용 후 없을 때만 네트워크 호출.
🤖 Prompt for AI Agents
In
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
around lines 397 to 413, the function builds a web search URL without
URL-encoding store titles and always calls the repository even if data is
already present; change it to first try
_menusOnPin.value?.firstOrNull()?.storeTitle and use that when non-null/blank to
avoid the network request, otherwise call mapRepository.getMapDetail(mapId);
before concatenating to baseUrl, URL-encode the storeTitle (e.g., via
URLEncoder.encode(title, "UTF-8")) and handle null/blank safely to return an
empty string on failure.

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

♻️ Duplicate comments (3)
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (3)

271-273: 로그에 전체 결과 객체 출력 지양(개인정보/노이즈)

과거 코멘트와 동일 이슈입니다. 결과 수만 남기세요.

다음 패치 제안:

-                    Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: $result")
+                    Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: ${result.size}건")

275-297: 검색 후 전체 목록 재조회: 캐시 우선 + 실패시 graceful fallback + IO 분리

불필요한 네트워크 왕복과 중첩 콜백입니다. 캐시(_myMenus) 우선 사용하고, 필요 시 한 번만 조회하세요. 히스토리 반영 호출도 실패 로그 포함해 IO에서 처리 권장.

-                    // 전체 메뉴 목록을 다시 가져온 후 필터링
-                    val allMenusResponse = mapRepository.getMap()
-                    allMenusResponse.onSuccess { allMenus ->
-                        if (allMenus != null) {
-                            // 전체 메뉴 중에서 검색 결과와 일치하는 것들만 필터링
-                            _myMenus.value = allMenus.filter { menu ->
-                                result.any { searchResult -> searchResult.mapId == menu.mapId }
-                            }
-                            // 검색 결과의 첫 번째 항목을 활성화 상태로 설정
-                            _activeMapId.value = result.firstOrNull()?.mapId
-                            showSearchResultOnMap()
-                            // 첫 번째 검색 결과의 상세 정보를 가져와서 바텀시트에 표시
-                            _activeMapId.value?.let { mapId ->
-                                getMapDetail(mapId)
-                            }
-                            // 검색 결과를 검색 기록에 반영
-                            if (result.firstOrNull()?.menuId != null){
-                                mapRepository.getMapMenuDetail(result.first().menuId)
-                                Log.d("SearchMenuViewModel", "검색 기록에 반영: ${result.first().menuId}")
-                            }
-                        }
-                    }
+                    // 캐시 우선, 없으면 1회 조회(+IO)
+                    val cached = _myMenus.value
+                    val allMenus = if (!cached.isNullOrEmpty()) cached else withContext(Dispatchers.IO) {
+                        mapRepository.getMap().getOrElse {
+                            Log.w("SearchMenuViewModel", "전체 메뉴 조회 실패(캐시 사용 불가): ${it.message}")
+                            null
+                        }
+                    } ?: emptyList()
+
+                    if (allMenus.isEmpty()) {
+                        Log.d("SearchMenuViewModel", "표시할 메뉴가 없습니다.")
+                        return@launch
+                    }
+
+                    _myMenus.value = allMenus.filter { menu ->
+                        result.any { searchResult -> searchResult.mapId == menu.mapId }
+                    }
+                    _activeMapId.value = result.firstOrNull()?.mapId
+                    showSearchResultOnMap()
+                    _activeMapId.value?.let { getMapDetail(it) }
+
+                    // 서버 히스토리 반영(성공/실패 로그, IO)
+                    result.firstOrNull()?.menuId?.let { id ->
+                        withContext(Dispatchers.IO) {
+                            mapRepository.getMapMenuDetail(id)
+                                .onFailure { Log.w("SearchMenuViewModel", "검색 기록 반영 실패(menuId=$id): ${it.message}") }
+                        }
+                    }

401-419: 네이버 검색 쿼리: URL 인코딩 누락 + 캐시 우선 사용

  • 공백/특수문자 인코딩 필요.
  • 이미 _menusOnPin에 제목이 있으면 네트워크 재호출 불필요.
-    suspend fun getWebSearchQuery(mapId: Long): String {
-        val baseUrl = "https://map.naver.com/p/search/"
-        val response = mapRepository.getMapDetail(mapId)
-        return response.fold(
-            onSuccess = { menuList ->
-                if (menuList.isNullOrEmpty()) {
-                    Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: 메뉴가 없습니다.")
-                    ""
-                } else {
-                    baseUrl + menuList.first().storeTitle
-                }
-            },
-            onFailure = { 
-                Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
-                ""
-            }
-        )
-    }
+    suspend fun getWebSearchQuery(mapId: Long): String {
+        val baseUrl = "https://map.naver.com/p/search/"
+        // 캐시 우선
+        val cachedTitle = _menusOnPin.value?.firstOrNull()?.storeTitle?.takeIf { it.isNotBlank() }
+        val title = cachedTitle ?: withContext(Dispatchers.IO) {
+            mapRepository.getMapDetail(mapId).getOrElse {
+                Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
+                emptyList()
+            }.firstOrNull()?.storeTitle
+        }
+        return title?.let {
+            val encoded = java.net.URLEncoder.encode(
+                it,
+                java.nio.charset.StandardCharsets.UTF_8.name()
+            )
+            baseUrl + encoded
+        } ?: ""
+    }
🧹 Nitpick comments (3)
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (3)

378-393: 카메라 이동 대상: 첫 번째 → 현재 중심과 가장 가까운 결과로

UX 측면에서 현재 뷰 중심과 가장 가까운 결과로 이동이 자연스럽습니다. 로그도 “N건” 정도만 남기면 충분.

-                // 첫 번째 검색 결과로 카메라 이동 TODO: 현재 위치랑 가까운 결과로 이동
-                moveCamera(menus[0].mapY, menus[0].mapX)
+                // 현재 중심과 가장 가까운 결과로 이동
+                val target = currentCenter.value?.let { c ->
+                    menus.minByOrNull {
+                        val dy = it.mapY - c.latitude
+                        val dx = it.mapX - c.longitude
+                        dy * dy + dx * dx
+                    }
+                } ?: menus.first()
+                moveCamera(target.mapY, target.mapX)

395-399: clearActiveMapId 시 시트 데이터도 비우기 권장

활성화 해제 후 바텀시트에 이전 핀의 메뉴가 남아있을 수 있습니다. 함께 초기화하면 혼동 방지됩니다.

     fun clearActiveMapId() {
         _activeMapId.value = null
         refreshMarkers()
+        _menusOnPin.value = emptyList()
     }

204-223: setOnLabelClickListener를 라벨 추가 시마다 재설정하지 마세요

마커 추가 루프마다 리스너를 재등록하면 마지막 등록만 유효하고 불필요한 작업이 반복됩니다. 초기화 시 한 번만 설정하고, 클릭 시 좌표/태그로 식별하세요.

예시(초기화 시 1회 설정):

// initializeMap 내부 어딘가에서 1회
mapController.kakaoMap.value?.setOnLabelClickListener { _, _, label ->
    val lat = label.position.latitude
    val lng = label.position.longitude
    _myMenus.value?.find { it.mapY == lat && it.mapX == lng }?.let { m ->
        _activeMapId.value = m.mapId
        refreshMarkers()
        getMapDetail(m.mapId)
        moveCamera(lat, lng)
    }
    true
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 82fac6b and 9f6b258.

📒 Files selected for processing (1)
  • app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt (3)
app/src/main/java/com/kuit/ourmenu/ui/addmenu/viewmodel/AddMenuViewModel.kt (4)
  • showSearchResultOnMap (247-260)
  • clearMarkers (171-175)
  • addMarker (147-168)
  • moveCamera (114-121)
app/src/main/java/com/kuit/ourmenu/data/service/MapService.kt (2)
  • getMapDetail (16-57)
  • getMapDetail (18-21)
app/src/main/java/com/kuit/ourmenu/data/repository/MapRepository.kt (1)
  • getMapDetail (12-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build

Comment on lines +342 to 371
// 먼저 전체 메뉴를 가져옴
val myMenusResponse = mapRepository.getMap()
myMenusResponse.onSuccess { menus ->
if (menus != null){
val allMenus = menus
Log.d("SearchMenuViewModel", "menuId로 메뉴 정보 요청: $menuId")
val menuDetailResponse = mapRepository.getMapMenuDetail(menuId)
menuDetailResponse.onSuccess { menuDetail ->
Log.d("SearchMenuViewModel", "메뉴 상세 조회 성공: $menuDetail")
// 검색 기록에서 해당 menuId를 가진 항목 찾기
searchHistory.value?.find { it.menuId == menuId }?.let { historyItem ->
Log.d("SearchMenuViewModel", "검색 기록에서 찾은 mapId: ${historyItem.mapId}")
// 가져온 전체 메뉴에서 필터링
_myMenus.value = allMenus.filter { menu ->
menu.mapId == historyItem.mapId
}
// 해당 mapId를 활성화 상태로 설정
_activeMapId.value = historyItem.mapId
// 지도에 검색 결과 표시
showSearchResultOnMap()
// 메뉴 상세 정보를 바텀시트에 표시하기 위해 설정
getMapDetail(historyItem.mapId)
}
}.onFailure {
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
}
}
}.onFailure {
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
Log.d("SearchMenuViewModel", "내 메뉴 조회 실패: ${it.message}")
}
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

getMapMenuDetail: 결과 미사용 + 히스토리 미존재 fallback 부재 + 캐시 미활용

  • menuDetail 결과를 로그만 하고 활용하지 않습니다.
  • searchHistory에 없을 때를 대비한 fallback(mapId) 분기가 없습니다.
  • 매번 전체 목록 재조회 대신 캐시 우선.
-            // 먼저 전체 메뉴를 가져옴
-            val myMenusResponse = mapRepository.getMap()
-            myMenusResponse.onSuccess { menus ->
-                if (menus != null){
-                    val allMenus = menus
-                    Log.d("SearchMenuViewModel", "menuId로 메뉴 정보 요청: $menuId")
-                    val menuDetailResponse = mapRepository.getMapMenuDetail(menuId)
-                    menuDetailResponse.onSuccess { menuDetail ->
-                        Log.d("SearchMenuViewModel", "메뉴 상세 조회 성공: $menuDetail")
-                        // 검색 기록에서 해당 menuId를 가진 항목 찾기
-                        searchHistory.value?.find { it.menuId == menuId }?.let { historyItem ->
-                            Log.d("SearchMenuViewModel", "검색 기록에서 찾은 mapId: ${historyItem.mapId}")
-                            // 가져온 전체 메뉴에서 필터링
-                            _myMenus.value = allMenus.filter { menu ->
-                                menu.mapId == historyItem.mapId
-                            }
-                            // 해당 mapId를 활성화 상태로 설정
-                            _activeMapId.value = historyItem.mapId
-                            // 지도에 검색 결과 표시
-                            showSearchResultOnMap()
-                            // 메뉴 상세 정보를 바텀시트에 표시하기 위해 설정
-                            getMapDetail(historyItem.mapId)
-                        }
-                    }.onFailure {
-                        Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
-                    }
-                }
-            }.onFailure {
-                Log.d("SearchMenuViewModel", "내 메뉴 조회 실패: ${it.message}")
-            }
+            // 캐시 우선
+            val cached = _myMenus.value
+            val allMenus = if (!cached.isNullOrEmpty()) cached else withContext(Dispatchers.IO) {
+                mapRepository.getMap().getOrElse {
+                    Log.w("SearchMenuViewModel", "내 메뉴 조회 실패(캐시 사용 불가): ${it.message}")
+                    null
+                }
+            } ?: emptyList()
+            if (allMenus.isEmpty()) {
+                Log.d("SearchMenuViewModel", "표시할 메뉴가 없습니다.")
+                return@launch
+            }
+
+            Log.d("SearchMenuViewModel", "menuId로 메뉴 정보 요청: $menuId")
+            val historyItem = searchHistory.value?.find { it.menuId == menuId }
+            val detailResult = withContext(Dispatchers.IO) { mapRepository.getMapMenuDetail(menuId) }
+
+            // 히스토리 우선, 없으면 상세의 mapId로 fallback
+            val targetMapId: Long? = historyItem?.mapId
+                ?: detailResult.getOrNull()?.mapId
+
+            if (targetMapId == null) {
+                Log.w("SearchMenuViewModel", "대상 mapId를 찾지 못했습니다(menuId=$menuId).")
+                return@launch
+            }
+
+            _myMenus.value = allMenus.filter { it.mapId == targetMapId }
+            _activeMapId.value = targetMapId
+            showSearchResultOnMap()
+            getMapDetail(targetMapId)
+
+            detailResult.onFailure { Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}") }

검증 필요:

  • 만약 mapId가 없다면, 상세 응답에서 mapId에 해당하는 필드를 알려주세요. 그에 맞추어 detailResult.getOrNull()?.<field>로 교체하겠습니다.

🏁 Script executed:

#!/bin/bash
# MapMenuDetailResponse에 mapId 존재 여부 확인
rg -n --iglob '*.kt' "data class\s+MapMenuDetailResponse" -C3
rg -n --iglob '*.kt' "\bMapMenuDetailResponse\b" -C2

Length of output: 2566


🏁 Script executed:

#!/bin/bash
# Inspect MapMenuDetailResponse fields to confirm presence of mapId
sed -n '1,200p' app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapMenuDetailResponse.kt

Length of output: 914


SearchMenuViewModel 캐시 우선 + fallback mapId 로직 적용

  • SearchMenuViewModel.kt (342–371)에서 myMenus 캐시 사용 후, searchHistory 미존재 시 detailResult.getOrNull()?.mapId 로 fallback하는 제안 로직을 적용하세요.
  • MapMenuDetailResponse@SerialName("mapId") val mapId: Long 필드가 있어 해당 필드를 바로 사용할 수 있습니다 (app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapMenuDetailResponse.kt).
🤖 Prompt for AI Agents
In
app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
around lines 342 to 371, update the logic that handles myMenus and menuDetail so
it first uses cached myMenus (as already fetched) and when no matching
searchHistory item exists, fall back to the mapId from the menu detail response
(detailResult.getOrNull()?.mapId) to filter and activate the map; specifically,
after menuDetailResponse.onSuccess, find a historyItem as now, and if null use
menuDetail?.mapId (from MapMenuDetailResponse) as fallback to set _myMenus
(filter by that mapId), set _activeMapId, call showSearchResultOnMap(), and
getMapDetail(mapId); preserve existing logging and onFailure handlers.

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.

LGTM :)

Comment on lines +139 to +148
BackHandler(enabled = showSearchBackground || showBottomSheet) {
if (showSearchBackground) {
if (searchBarFocused) focusManager.clearFocus()
searchActionDone = false
showSearchBackground = false
searchText = ""
} else if (showBottomSheet) {
showBottomSheet = false
viewModel.clearActiveMapId()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

@casper-jr casper-jr merged commit cce63d7 into OurMenu:develop Sep 1, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat Good for newcomers ⚡️ fix Extra attention is needed 👌 OK Merge 🦦재우

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 지도 메뉴의 검색 기능 구현

2 participants