Skip to content

Commit cce63d7

Browse files
authored
Merge pull request #85 from casper-jr/feat/mapsearch
[FEAT] 메뉴 검색시 지도에 핀 표시 로직, 검색 결과 조회 기능 구현
2 parents 517219b + 9f6b258 commit cce63d7

6 files changed

Lines changed: 158 additions & 49 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable
55

66
@Serializable
77
data class MapSearchHistoryResponse(
8+
@SerialName("mapId")
9+
val mapId: Long,
810
@SerialName("menuId")
911
val menuId: Long,
1012
@SerialName("menuTitle")

app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import kotlinx.serialization.Serializable
55

66
@Serializable
77
data class MapSearchResponse(
8+
@SerialName("mapId")
9+
val mapId: Long,
10+
@SerialName("menuId")
11+
val menuId: Long,
812
@SerialName("menuTitle")
913
val menuTitle: String,
1014
@SerialName("storeTitle")

app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row
1212
import androidx.compose.foundation.layout.fillMaxWidth
1313
import androidx.compose.foundation.layout.height
1414
import androidx.compose.foundation.layout.padding
15-
import androidx.compose.foundation.layout.size
1615
import androidx.compose.foundation.shape.RoundedCornerShape
1716
import androidx.compose.material3.Text
1817
import androidx.compose.runtime.Composable
@@ -137,25 +136,28 @@ fun MenuInfoImage(
137136
) {
138137
val imgUrls = menuInfoData.menuImgUrls
139138

140-
Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) {
139+
Row(
140+
modifier = modifier,
141+
horizontalArrangement = Arrangement.spacedBy(4.dp)
142+
) {
141143
for (i in 0 until 3) {
142144
Image(
143145
painter = if (i < imgUrls.size && imgUrls[i].isNotEmpty()) {
144146
rememberAsyncImagePainter(
145147
model = ImageRequest.Builder(LocalPlatformContext.current)
146148
.data(imgUrls[i])
147-
.size(104, 80)
149+
.size(108, 80)
148150
.build()
149151
)
150152
} else {
151153
painterResource(R.drawable.img_dummy_menu)
152154
},
153155
contentDescription = null,
154156
modifier = Modifier
155-
.size(104.dp, 80.dp)
157+
.weight(1f)
158+
.height(80.dp)
156159
.clip(shape = RoundedCornerShape(8.dp))
157160
)
158-
// if (i != 2) Spacer(modifier = Modifier.padding(end = 4.dp))
159161
}
160162
}
161163
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,21 +162,24 @@ private fun SearchHistoryPreview() {
162162
SearchHistoryList(
163163
historyList = listOf(
164164
MapSearchHistoryResponse(
165+
mapId = 1,
166+
menuId = 1,
165167
menuTitle = "피자",
166168
storeTitle = "피자헛",
167-
menuId = 1,
168169
storeAddress = "서울특별시 강남구 역삼동 123-4"
169170
),
170171
MapSearchHistoryResponse(
172+
mapId = 2,
173+
menuId = 2,
171174
menuTitle = "치킨",
172175
storeTitle = "굽네치킨",
173-
menuId = 2,
174176
storeAddress = "서울특별시 강남구 역삼동 456-7"
175177
),
176178
MapSearchHistoryResponse(
179+
mapId = 3,
180+
menuId = 3,
177181
menuTitle = "햄버거",
178182
storeTitle = "맥도날드",
179-
menuId = 3,
180183
storeAddress = "서울특별시 강남구 역삼동 987-6"
181184
)
182185
),

app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.kuit.ourmenu.ui.searchmenu.screen
22

33
import android.Manifest
4+
import android.content.Intent
45
import android.util.Log
56
import androidx.activity.compose.BackHandler
67
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -32,6 +33,7 @@ import androidx.compose.ui.platform.LocalFocusManager
3233
import androidx.compose.ui.tooling.preview.Preview
3334
import androidx.compose.ui.unit.dp
3435
import androidx.compose.ui.viewinterop.AndroidView
36+
import androidx.core.net.toUri
3537
import androidx.hilt.navigation.compose.hiltViewModel
3638
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3739
import com.kuit.ourmenu.R
@@ -71,18 +73,22 @@ fun SearchMenuScreen(
7173

7274
// 지도 중심 좌표
7375
val currentCenter by viewModel.currentCenter.collectAsStateWithLifecycle()
74-
76+
7577
// 검색기록
7678
val searchHistory by viewModel.searchHistory.collectAsStateWithLifecycle()
77-
79+
7880
// 핀 위치에 해당하는 메뉴들
7981
val menusOnPin by viewModel.menusOnPin.collectAsStateWithLifecycle()
8082

83+
// 선택된 라벨
84+
val activeMapId by viewModel.activeMapId.collectAsStateWithLifecycle()
85+
8186
val density = LocalDensity.current
8287
val singleItemHeight = 300.dp // Fixed height for each item
8388

84-
LaunchedEffect(menusOnPin) {
85-
if (menusOnPin != null && menusOnPin?.isNotEmpty() == true) {
89+
// 메뉴핀이 선택되었을 때 바텀시트 상태 변경
90+
LaunchedEffect(menusOnPin, activeMapId) {
91+
if (activeMapId != null && menusOnPin != null && menusOnPin?.isNotEmpty() == true) {
8692
showBottomSheet = true
8793
}
8894
}
@@ -130,11 +136,16 @@ fun SearchMenuScreen(
130136
}
131137
}
132138

133-
BackHandler(enabled = showSearchBackground) {
134-
if (searchBarFocused) focusManager.clearFocus()
135-
searchActionDone = false
136-
showSearchBackground = false
137-
searchText = ""
139+
BackHandler(enabled = showSearchBackground || showBottomSheet) {
140+
if (showSearchBackground) {
141+
if (searchBarFocused) focusManager.clearFocus()
142+
searchActionDone = false
143+
showSearchBackground = false
144+
searchText = ""
145+
} else if (showBottomSheet) {
146+
showBottomSheet = false
147+
viewModel.clearActiveMapId()
148+
}
138149
}
139150

140151
BottomSheetScaffold(
@@ -160,7 +171,7 @@ fun SearchMenuScreen(
160171
}
161172
)
162173
},
163-
sheetPeekHeight = if(showBottomSheet) {
174+
sheetPeekHeight = if (showBottomSheet) {
164175
val itemCount = menusOnPin?.size ?: 0
165176
(singleItemHeight * itemCount) + dragHandleHeight
166177
} else 0.dp,
@@ -190,6 +201,7 @@ fun SearchMenuScreen(
190201
// 크롤링 기록 아이템 클릭시 동작
191202
viewModel.getMapMenuDetail(menuId)
192203
Log.d("SearchMenuScreen", "검색 기록 아이템 클릭: $menuId")
204+
focusManager.clearFocus()
193205
showSearchBackground = false
194206
showBottomSheet = true
195207
}
@@ -210,37 +222,54 @@ fun SearchMenuScreen(
210222
// onSearch 함수
211223
if (searchBarFocused) focusManager.clearFocus()
212224
searchActionDone = true
213-
225+
214226
// 검색 시 현재 지도 중심 좌표 사용
215227
if (searchText.isNotEmpty()) {
216228
// 검색 직전에 현재 지도 중심 좌표 업데이트
217229
viewModel.updateCurrentCenter()
218-
230+
219231
val center = viewModel.getCurrentCoordinates()
220232
if (center != null) {
221233
val (latitude, longitude) = center
222234
Log.d("SearchMenuScreen", "검색 위치: $latitude, $longitude")
223-
235+
224236
// 검색어와 현재 좌표로 스토어 정보 요청
225237
viewModel.getMapSearchResult(
226238
query = searchText,
227239
long = longitude,
228240
lat = latitude
229241
)
230-
242+
231243
showBottomSheet = true
232244
showSearchBackground = false
233245
}
246+
}else{
247+
Log.d("SearchMenuScreen", "검색어가 비어있습니다.")
248+
viewModel.getMyMenus()
249+
showBottomSheet = false
250+
showSearchBackground = false
234251
}
252+
searchText = ""
235253
}
236254

237255
GoToMapButton(
238256
modifier = Modifier
239257
.align(Alignment.BottomEnd)
240258
.padding(bottom = 16.dp, end = 20.dp),
241-
onClick = {
242-
// TODO: 임시로 설정해놓은 카메라 이동, 실제로는 네이버 지도에 해당 가게 검색 결과로 이동
243-
viewModel.moveCamera(37.5416, 127.0793)
259+
onClick = {
260+
// 네이버 지도에 해당 가게 검색 결과로 이동
261+
if (activeMapId == null) {
262+
Log.d("SearchMenuScreen", "활성화된 Map ID가 없습니다.")
263+
} else {
264+
scope.launch {
265+
val searchQuery = viewModel.getWebSearchQuery(activeMapId!!)
266+
if (searchQuery.isNotBlank()) {
267+
Log.d("SearchMenuScreen", "intent query: $searchQuery")
268+
val webIntent = Intent(Intent.ACTION_VIEW, searchQuery.toUri())
269+
context.startActivity(webIntent)
270+
}
271+
}
272+
}
244273
},
245274
)
246275
}
@@ -252,7 +281,7 @@ fun SearchMenuScreen(
252281
@Composable
253282
private fun SearchMenuScreenPreview() {
254283
SearchMenuScreen(
255-
){
284+
) {
256285

257286
}
258287
}

app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt

Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,33 @@ class SearchMenuViewModel @Inject constructor(
268268

269269
response.onSuccess { result ->
270270
if (result != null && result.isNotEmpty()) {
271-
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: ${result.size}")
271+
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 성공: $result")
272272
// 검색 결과 저장
273273
_searchResult.value = result
274+
275+
// 전체 메뉴 목록을 다시 가져온 후 필터링
276+
val allMenusResponse = mapRepository.getMap()
277+
allMenusResponse.onSuccess { allMenus ->
278+
if (allMenus != null) {
279+
// 전체 메뉴 중에서 검색 결과와 일치하는 것들만 필터링
280+
_myMenus.value = allMenus.filter { menu ->
281+
result.any { searchResult -> searchResult.mapId == menu.mapId }
282+
}
283+
// 검색 결과의 첫 번째 항목을 활성화 상태로 설정
284+
_activeMapId.value = result.firstOrNull()?.mapId
285+
showSearchResultOnMap()
286+
// 첫 번째 검색 결과의 상세 정보를 가져와서 바텀시트에 표시
287+
_activeMapId.value?.let { mapId ->
288+
getMapDetail(mapId)
289+
}
290+
// 검색 결과를 검색 기록에 반영
291+
if (result.firstOrNull()?.menuId != null){
292+
mapRepository.getMapMenuDetail(result.first().menuId)
293+
Log.d("SearchMenuViewModel", "검색 기록에 반영: ${result.first().menuId}")
294+
}
295+
296+
}
297+
}
274298
}
275299
}.onFailure {
276300
Log.d("SearchMenuViewModel", "등록 메뉴 정보 조회 실패: ${it.message}")
@@ -315,37 +339,82 @@ class SearchMenuViewModel @Inject constructor(
315339

316340
fun getMapMenuDetail(menuId: Long) {
317341
viewModelScope.launch {
318-
val response = mapRepository.getMapMenuDetail(menuId)
319-
response.onSuccess { menuDetail ->
320-
Log.d("SearchMenuViewModel", "메뉴 상세 조회 성공: $menuDetail")
321-
322-
// myMenus에서 해당 menuId를 가진 메뉴의 위치 정보 찾기
323-
myMenus.value?.find { it.mapId == menuId }?.let { menu ->
324-
// 해당 위치로 카메라 이동
325-
moveCamera(menu.mapY, menu.mapX)
326-
// 해당 핀을 활성화 상태로 변경
327-
_activeMapId.value = menuId
328-
refreshMarkers()
329-
// 메뉴 상세 정보를 바텀시트에 표시하기 위해 설정
330-
getMapDetail(menuId)
342+
// 먼저 전체 메뉴를 가져옴
343+
val myMenusResponse = mapRepository.getMap()
344+
myMenusResponse.onSuccess { menus ->
345+
if (menus != null){
346+
val allMenus = menus
347+
Log.d("SearchMenuViewModel", "menuId로 메뉴 정보 요청: $menuId")
348+
val menuDetailResponse = mapRepository.getMapMenuDetail(menuId)
349+
menuDetailResponse.onSuccess { menuDetail ->
350+
Log.d("SearchMenuViewModel", "메뉴 상세 조회 성공: $menuDetail")
351+
// 검색 기록에서 해당 menuId를 가진 항목 찾기
352+
searchHistory.value?.find { it.menuId == menuId }?.let { historyItem ->
353+
Log.d("SearchMenuViewModel", "검색 기록에서 찾은 mapId: ${historyItem.mapId}")
354+
// 가져온 전체 메뉴에서 필터링
355+
_myMenus.value = allMenus.filter { menu ->
356+
menu.mapId == historyItem.mapId
357+
}
358+
// 해당 mapId를 활성화 상태로 설정
359+
_activeMapId.value = historyItem.mapId
360+
// 지도에 검색 결과 표시
361+
showSearchResultOnMap()
362+
// 메뉴 상세 정보를 바텀시트에 표시하기 위해 설정
363+
getMapDetail(historyItem.mapId)
364+
}
365+
}.onFailure {
366+
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
367+
}
331368
}
332369
}.onFailure {
333-
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
370+
Log.d("SearchMenuViewModel", "내 메뉴 조회 실패: ${it.message}")
334371
}
335372
}
336373
}
337374

338375
// 지도에 검색 결과 핀 추가
339376
fun showSearchResultOnMap() {
340377
clearMarkers()
341-
myMenus.value?.forEach { store ->
342-
addMarker(store, store.mapId == _activeMapId.value)
343-
Log.d(
344-
"SearchMenuViewModel",
345-
"mapId: ${store.mapId} lat: (${store.mapY}, long: ${store.mapX})"
346-
)
378+
_myMenus.value?.let { menus ->
379+
if (menus.isNotEmpty()) {
380+
menus.forEach { store ->
381+
addMarker(store, store.mapId == _activeMapId.value)
382+
Log.d(
383+
"SearchMenuViewModel",
384+
"mapId: ${store.mapId} lat: (${store.mapY}, long: ${store.mapX})"
385+
)
386+
}
387+
// 첫 번째 검색 결과로 카메라 이동 TODO: 현재 위치랑 가까운 결과로 이동
388+
moveCamera(menus[0].mapY, menus[0].mapX)
389+
} else {
390+
Log.d("SearchMenuViewModel", "검색 결과가 없습니다.")
391+
}
347392
}
348-
// 첫 번째 검색 결과로 카메라 이동 TODO: 현재 위치랑 가까운 결과로 이동
349-
myMenus.value?.get(0)?.let { moveCamera(it.mapY, it.mapX) }
393+
}
394+
395+
// 활성화된 맵 ID를 초기화하고 마커를 다시 그림
396+
fun clearActiveMapId() {
397+
_activeMapId.value = null
398+
refreshMarkers()
399+
}
400+
401+
// 네이버맵 이동을 위한 가게명 조회
402+
suspend fun getWebSearchQuery(mapId: Long): String {
403+
val baseUrl = "https://map.naver.com/p/search/"
404+
val response = mapRepository.getMapDetail(mapId)
405+
return response.fold(
406+
onSuccess = { menuList ->
407+
if (menuList.isNullOrEmpty()) {
408+
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: 메뉴가 없습니다.")
409+
""
410+
} else {
411+
baseUrl + menuList.first().storeTitle
412+
}
413+
},
414+
onFailure = {
415+
Log.d("SearchMenuViewModel", "메뉴 상세 조회 실패: ${it.message}")
416+
""
417+
}
418+
)
350419
}
351420
}

0 commit comments

Comments
 (0)