Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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,29 @@ 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)
}
}
}
Comment on lines 275 to 297
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.

}
}.onFailure {
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 실패: ${it.message}")
Expand Down Expand Up @@ -315,37 +335,81 @@ 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
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 338 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 호출 결과를 사용하지 않음(불필요 네트워크 왕복)

현재 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 +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.

}
}