Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable

@Serializable
data class MapSearchHistoryResponse(
@SerialName("mapId")
val mapId: Long,
@SerialName("menuId")
val menuId: Long,
@SerialName("menuTitle")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import kotlinx.serialization.Serializable

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

@SerialName("menuTitle")
val menuTitle: String,
@SerialName("storeTitle")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -137,25 +136,28 @@ fun MenuInfoImage(
) {
val imgUrls = menuInfoData.menuImgUrls

Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
for (i in 0 until 3) {
Image(
painter = if (i < imgUrls.size && imgUrls[i].isNotEmpty()) {
rememberAsyncImagePainter(
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.

.build()
)
} else {
painterResource(R.drawable.img_dummy_menu)
},
contentDescription = null,
modifier = Modifier
.size(104.dp, 80.dp)
.weight(1f)
.height(80.dp)
.clip(shape = RoundedCornerShape(8.dp))
)
// if (i != 2) Spacer(modifier = Modifier.padding(end = 4.dp))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,21 +162,24 @@ private fun SearchHistoryPreview() {
SearchHistoryList(
historyList = listOf(
MapSearchHistoryResponse(
mapId = 1,
menuId = 1,
menuTitle = "피자",
storeTitle = "피자헛",
menuId = 1,
storeAddress = "서울특별시 강남구 역삼동 123-4"
),
MapSearchHistoryResponse(
mapId = 2,
menuId = 2,
menuTitle = "치킨",
storeTitle = "굽네치킨",
menuId = 2,
storeAddress = "서울특별시 강남구 역삼동 456-7"
),
MapSearchHistoryResponse(
mapId = 3,
menuId = 3,
menuTitle = "햄버거",
storeTitle = "맥도날드",
menuId = 3,
storeAddress = "서울특별시 강남구 역삼동 987-6"
)
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kuit.ourmenu.ui.searchmenu.screen

import android.Manifest
import android.content.Intent
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.interaction.MutableInteractionSource
Expand Down Expand Up @@ -32,6 +33,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.kuit.ourmenu.R
Expand Down Expand Up @@ -71,18 +73,22 @@ fun SearchMenuScreen(

// 지도 중심 좌표
val currentCenter by viewModel.currentCenter.collectAsStateWithLifecycle()

// 검색기록
val searchHistory by viewModel.searchHistory.collectAsStateWithLifecycle()

// 핀 위치에 해당하는 메뉴들
val menusOnPin by viewModel.menusOnPin.collectAsStateWithLifecycle()

// 선택된 라벨
val activeMapId by viewModel.activeMapId.collectAsStateWithLifecycle()

val density = LocalDensity.current
val singleItemHeight = 300.dp // Fixed height for each item

LaunchedEffect(menusOnPin) {
if (menusOnPin != null && menusOnPin?.isNotEmpty() == true) {
// 메뉴핀이 선택되었을 때 바텀시트 상태 변경
LaunchedEffect(menusOnPin, activeMapId) {
if (activeMapId != null && menusOnPin != null && menusOnPin?.isNotEmpty() == true) {
showBottomSheet = true
}
}
Expand Down Expand Up @@ -130,11 +136,16 @@ fun SearchMenuScreen(
}
}

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

Choose a reason for hiding this comment

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

👍

}

BottomSheetScaffold(
Expand All @@ -160,7 +171,7 @@ fun SearchMenuScreen(
}
)
},
sheetPeekHeight = if(showBottomSheet) {
sheetPeekHeight = if (showBottomSheet) {
val itemCount = menusOnPin?.size ?: 0
(singleItemHeight * itemCount) + dragHandleHeight
} else 0.dp,
Comment on lines +174 to 177
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.

Expand Down Expand Up @@ -190,6 +201,7 @@ fun SearchMenuScreen(
// 크롤링 기록 아이템 클릭시 동작
viewModel.getMapMenuDetail(menuId)
Log.d("SearchMenuScreen", "검색 기록 아이템 클릭: $menuId")
focusManager.clearFocus()
showSearchBackground = false
showBottomSheet = true
}
Expand All @@ -210,37 +222,54 @@ fun SearchMenuScreen(
// onSearch 함수
if (searchBarFocused) focusManager.clearFocus()
searchActionDone = true

// 검색 시 현재 지도 중심 좌표 사용
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{
Comment on lines 226 to +246
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).

Log.d("SearchMenuScreen", "검색어가 비어있습니다.")
viewModel.getMyMenus()
showBottomSheet = false
showSearchBackground = false
}
searchText = ""
}

GoToMapButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = 16.dp, end = 20.dp),
onClick = {
// TODO: 임시로 설정해놓은 카메라 이동, 실제로는 네이버 지도에 해당 가게 검색 결과로 이동
viewModel.moveCamera(37.5416, 127.0793)
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)
}
}
}
},
Comment on lines +259 to 273
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")
}
}
}
},

)
}
Expand All @@ -252,7 +281,7 @@ fun SearchMenuScreen(
@Composable
private fun SearchMenuScreenPreview() {
SearchMenuScreen(
){
) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,33 @@ class SearchMenuViewModel @Inject constructor(

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

Comment on lines 269 to +274
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.

// 전체 메뉴 목록을 다시 가져온 후 필터링
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}")
}

}
}
}
}.onFailure {
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 실패: ${it.message}")
Expand Down Expand Up @@ -315,37 +339,82 @@ class SearchMenuViewModel @Inject constructor(

fun getMapMenuDetail(menuId: Long) {
viewModelScope.launch {
val response = mapRepository.getMapMenuDetail(menuId)
response.onSuccess { menuDetail ->
Log.d("SearchMenuViewModel", "메뉴 상세 조회 성공: $menuDetail")

// myMenus에서 해당 menuId를 가진 메뉴의 위치 정보 찾기
myMenus.value?.find { it.mapId == menuId }?.let { menu ->
// 해당 위치로 카메라 이동
moveCamera(menu.mapY, menu.mapX)
// 해당 핀을 활성화 상태로 변경
_activeMapId.value = menuId
refreshMarkers()
// 메뉴 상세 정보를 바텀시트에 표시하기 위해 설정
getMapDetail(menuId)
// 먼저 전체 메뉴를 가져옴
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}")
}
Comment on lines +342 to 371
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.

}
}

// 지도에 검색 결과 핀 추가
fun showSearchResultOnMap() {
clearMarkers()
myMenus.value?.forEach { store ->
addMarker(store, store.mapId == _activeMapId.value)
Log.d(
"SearchMenuViewModel",
"mapId: ${store.mapId} lat: (${store.mapY}, long: ${store.mapX})"
)
_myMenus.value?.let { menus ->
if (menus.isNotEmpty()) {
menus.forEach { store ->
addMarker(store, store.mapId == _activeMapId.value)
Log.d(
"SearchMenuViewModel",
"mapId: ${store.mapId} lat: (${store.mapY}, long: ${store.mapX})"
)
}
// 첫 번째 검색 결과로 카메라 이동 TODO: 현재 위치랑 가까운 결과로 이동
moveCamera(menus[0].mapY, menus[0].mapX)
} else {
Log.d("SearchMenuViewModel", "검색 결과가 없습니다.")
}
}
// 첫 번째 검색 결과로 카메라 이동 TODO: 현재 위치랑 가까운 결과로 이동
myMenus.value?.get(0)?.let { moveCamera(it.mapY, it.mapX) }
}

// 활성화된 맵 ID를 초기화하고 마커를 다시 그림
fun clearActiveMapId() {
_activeMapId.value = null
refreshMarkers()
}

// 네이버맵 이동을 위한 가게명 조회
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}")
""
}
)
Comment on lines +402 to +418
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.

}
}