Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6ff95c0
feat: 제휴지도 학과/전체 토글 디폴트값 변경 및 토글 UI 위치 변경
kangyuri1114 Nov 19, 2025
f1d7e6f
feat: PartnershipFilterToggle에서 toggleItem 생성방식 변경 및 UI수정, 불필요 코드 삭제
kangyuri1114 Nov 19, 2025
a8b14fb
delete: 사용하지 않는 위치 권한 코드 삭제
kangyuri1114 Nov 21, 2025
b54287c
feat: compose 라이브러리 버전 업데이트
kangyuri1114 Nov 21, 2025
30eaefc
feat: compose router-screen 분리
kangyuri1114 Nov 21, 2025
f57a713
feat: 학과 선택을 하지 않은 경우, default 토글이 "전체"로 수정. 학과 토글 선택 시 바텀시트 보여주기
kangyuri1114 Nov 24, 2025
346e0ed
feat: 제휴지도 학과/전체 토글 디폴트값 변경 및 토글 UI 위치 변경
kangyuri1114 Nov 19, 2025
8c5adb9
feat: PartnershipFilterToggle에서 toggleItem 생성방식 변경 및 UI수정, 불필요 코드 삭제
kangyuri1114 Nov 19, 2025
40d912c
delete: 사용하지 않는 위치 권한 코드 삭제
kangyuri1114 Nov 21, 2025
23bfca2
feat: compose 라이브러리 버전 업데이트
kangyuri1114 Nov 21, 2025
c3884e0
feat: compose router-screen 분리
kangyuri1114 Nov 21, 2025
037cef4
feat: 학과 선택을 하지 않은 경우, default 토글이 "전체"로 수정. 학과 토글 선택 시 바텀시트 보여주기
kangyuri1114 Nov 24, 2025
5e2ffc9
Merge remote-tracking branch 'origin/feat/replace-map-toggle-default'…
kangyuri1114 Nov 24, 2025
6ae0eac
feat: compose 버전 업데이트 롤백
kangyuri1114 Nov 24, 2025
d8e2c78
feat: 학과 정보가 업데이트될 때마다 토글 상태 없데이트하도록 key 변경
kangyuri1114 Nov 25, 2025
77487ef
feat: init 내부에서 전체 제휴정보 load하는 코드 제거(compose LaunchedEffect로 이동)
kangyuri1114 Nov 25, 2025
c2006c4
feat: BottomSheet 표시는 View의 SheetState로만 관리 (ViewModel은 데이터만 제공), 제휴정…
kangyuri1114 Nov 25, 2025
be080a7
feat: Domain 모델(RestaurantType)을 UI 모델(PlaceType)로 변환 로직을 뷰모델로 이동
kangyuri1114 Nov 25, 2025
7bf29b2
feat: 토글 필터 상태, 이벤트 로거를 ViewModel로 이동. departmentId, collegeId flow 방…
kangyuri1114 Nov 25, 2025
84bbc3d
[Hotfix] Release에서 발생하던 문제 해결 및 3.1.8 릴리즈 (#418)
PeraSite Nov 25, 2025
10036e9
chore: material 의존성 제거 (#423)
HI-JIN2 Nov 25, 2025
8a974e2
feat: 코드 리뷰 반영 (네이밍 변경 및 scope 전달 -> 람다 전달로 수정, 최초 정보 load 시 state co…
kangyuri1114 Nov 26, 2025
052b826
feat: 제휴정보 토글 변경 시 선택했던 식당의 제휴정보 state 초기화
kangyuri1114 Nov 26, 2025
7d7731c
feat: MapScreen 접근 제어자 private -> internal 변경
kangyuri1114 Nov 26, 2025
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
5 changes: 2 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ android {
applicationId = "com.eatssu.android"
minSdk = 28
targetSdk = 35
versionCode = 45
versionName = "3.1.7"
versionCode = 46
versionName = "3.1.8"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -122,7 +122,6 @@ dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
implementation(libs.threetenabp)
implementation(libs.material.calendarview)
Expand Down
5 changes: 1 addition & 4 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@
# https://firebase.google.com/docs/database/android/start?hl=ko
-keepattributes Signature

-keep class com.eatssu.android.data.dto.** {
*;
}
-keep class com.eatssu.android.data.enums.** {
-keep class com.eatssu.android.data.remote.dto.** {
*;
}
-keep class com.eatssu.android.data.model.** {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.eatssu.common.enums.Restaurant
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.tasks.await
import timber.log.Timber
import javax.inject.Inject
Expand Down Expand Up @@ -60,8 +59,7 @@ class FirebaseRemoteConfigRepositoryImpl @Inject constructor(
private fun parseCafeteriaJson(json: String): List<RestaurantInfo> {
return try {
val gson = Gson()
val listType = object : TypeToken<List<RestaurantInfo>>() {}.type
val dtoList: List<RestaurantInfo> = gson.fromJson(json, listType)
val dtoList = gson.fromJson(json, Array<RestaurantInfo>::class.java) ?: emptyArray()

dtoList.map { dto ->
RestaurantInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.eatssu.android.presentation.map

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import com.eatssu.design_system.theme.EatssuTheme
import dagger.hilt.android.AndroidEntryPoint

Expand All @@ -25,7 +25,7 @@ class MapFragment : Fragment() {
return ComposeView(requireContext()).apply {
setContent {
EatssuTheme {
MapFragmentComposeView()
MapRoute()
}
}
}
Expand Down
188 changes: 111 additions & 77 deletions app/src/main/java/com/eatssu/android/presentation/map/MapFragmentView.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalNaverMapApi::class)

package com.eatssu.android.presentation.map

import android.Manifest
Expand All @@ -17,17 +19,16 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
Expand All @@ -49,7 +50,6 @@ import com.eatssu.android.presentation.map.component.DepartmentBottomSheet
import com.eatssu.android.presentation.map.component.FilterType
import com.eatssu.android.presentation.map.component.MapRestaurantBottomSheet
import com.eatssu.android.presentation.map.component.PartnershipFilterToggle
import com.eatssu.android.presentation.map.model.PlaceType
import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity
import com.eatssu.android.presentation.util.TrackScreenViewEvent
import com.eatssu.common.EventLogger
Expand All @@ -61,6 +61,7 @@ import com.eatssu.design_system.theme.EatssuTheme
import com.naver.maps.geometry.LatLng
import com.naver.maps.map.CameraPosition
import com.naver.maps.map.compose.Align
import com.naver.maps.map.compose.CameraPositionState
import com.naver.maps.map.compose.ExperimentalNaverMapApi
import com.naver.maps.map.compose.LocationTrackingMode
import com.naver.maps.map.compose.MapProperties
Expand All @@ -80,9 +81,8 @@ private const val DEFAULT_LONGITUDE = 126.95661313346206
private const val DEFAULT_ZOOM = 17.5
private const val PERMISSION_REQUEST_CODE = 1001

@OptIn(ExperimentalNaverMapApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MapFragmentComposeView(
fun MapRoute(
viewModel: MapViewModel = viewModel(),
mainViewModel: MainViewModel = viewModel()
) {
Expand All @@ -95,15 +95,15 @@ fun MapFragmentComposeView(
}

val mainUiState by mainViewModel.uiState.collectAsStateWithLifecycle()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val departmentSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val partnershipSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val context = LocalContext.current
val activity = remember(context) { context.findActivityOrNull() }
?: throw IllegalStateException("FusedLocationSource는 Activity에서만 사용할 수 있습니다.")
val scope = rememberCoroutineScope()
var selectedFilter by remember { mutableStateOf(FilterType.All) }

val departmentId = viewModel.departmentId
val collegeId = viewModel.collegeId
val departmentId by viewModel.departmentId.collectAsStateWithLifecycle()
val collegeId by viewModel.collegeId.collectAsStateWithLifecycle()

val cameraPositionState = rememberCameraPositionState {
position = CameraPosition(
Expand Down Expand Up @@ -165,38 +165,30 @@ fun MapFragmentComposeView(
// 상태 변화 감지해서 show/hide -> Scrim 잔존 문제 해결
LaunchedEffect(showUserDepartmentBottomSheet) {
if (showUserDepartmentBottomSheet) {
sheetState.show()
departmentSheetState.show()
} else {
sheetState.hide()
departmentSheetState.hide()
}
}

var hasLocationPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
)
// 필터 변경 결과에 따라 학과 입력 BottomSheet 표시
LaunchedEffect(mapState.filterChangeResult) {
if (mapState.filterChangeResult is MapState.FilterChangeResult.RequiresDepartment) {
departmentSheetState.show()
}
}

// 제휴 정보 토글 event
LaunchedEffect(selectedFilter) {
when (selectedFilter) {
FilterType.All -> {
viewModel.loadPartnerships()
EventLogger.clickMap()
}
FilterType.Mine -> {
viewModel.loadUserCollegePartnerships()

EventLogger.clickMapMine(collegeId, departmentId)
}
// 제휴 정보가 선택되면 BottomSheet 표시
LaunchedEffect(mapState.restaurantPartnershipInfo) {
if (mapState.restaurantPartnershipInfo != null) {
partnershipSheetState.show()
}
Timber.d("선택된 식당 제휴 정보: ${mapState.restaurantPartnershipInfo}")
}


// Screen View 기록
TrackScreenViewEvent(ScreenId.MAP_MAIN)


val lifecycleOwner = LocalLifecycleOwner.current

// onResume 시마다 학과 정보 반영
Expand All @@ -214,6 +206,56 @@ fun MapFragmentComposeView(
}
}

MapScreen(
mapState = mapState,
viewModel = viewModel,
cameraPositionState = cameraPositionState,
locationSource = locationSource,
departmentSheetState = departmentSheetState,
partnershipSheetState = partnershipSheetState,
showToast = { message ->
scope.launch {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
},
navigateToUserInfo = {
val intent = Intent(context, UserInfoActivity::class.java)
context.startActivity(intent)
},
onHideDepartmentSheet = {
scope.launch { departmentSheetState.hide() }
},
onHidePartnershipSheet = {
scope.launch { partnershipSheetState.hide() }
},
onSelectedFilterChange = { filter ->
viewModel.setFilter(filter)
},
departmentId = departmentId,
collegeId = collegeId,
departmentName = departmentName,
selectedFilter = mapState.selectedFilter,
)
}

@Composable
internal fun MapScreen(
mapState: MapState,
viewModel: MapViewModel,
cameraPositionState: CameraPositionState,
locationSource: FusedLocationSource,
departmentSheetState: SheetState,
partnershipSheetState: SheetState,
showToast: (String) -> Unit,
navigateToUserInfo: () -> Unit,
onHideDepartmentSheet: () -> Unit = {},
onHidePartnershipSheet: () -> Unit = {},
onSelectedFilterChange: (FilterType) -> Unit,
departmentId: Long,
collegeId: Long,
departmentName: String?,
selectedFilter: FilterType,
) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
Expand All @@ -232,40 +274,42 @@ fun MapFragmentComposeView(
) { innerPadding ->

// 학과 정보가 없을 때 보여줄 BottomSheet
if (sheetState.isVisible) {
if (departmentSheetState.isVisible) {
Timber.d("학과 정보가 없습니다. BottomSheet를 표시합니다.")

DepartmentBottomSheet(
onDismiss = { viewModel.toggleDepartmentBottomSheet() },
onDismiss = {
onHideDepartmentSheet()
},
onInputClick = {
viewModel.toggleDepartmentBottomSheet()
val intent = Intent(context, UserInfoActivity::class.java)
context.startActivity(intent)
onHideDepartmentSheet()
navigateToUserInfo()
},
sheetState = sheetState
sheetState = departmentSheetState
)
}

// 특정 식당에 대한 제휴 정보 BottomSheet
if (mapState.showPartnershipBottomSheet) {
if (partnershipSheetState.isVisible) {
mapState.restaurantPartnershipInfo?.let { info ->
EventLogger.clickPartnerRestaurant(
college = collegeId,
major = departmentId,
partnerRestaurantId = info.id.toLong()
)

MapRestaurantBottomSheet(
storeName = info.storeName,
placeType = when (info.restaurantType) {
RestaurantType.CAFE -> PlaceType.CAFE
RestaurantType.RESTAURANT -> PlaceType.RESTAURANT
RestaurantType.PUB -> PlaceType.PUB
else -> PlaceType.RESTAURANT
},
mapRestaurantList = mapState.restaurantInfoList,
onDismiss = { viewModel.togglePartnershipBottomSheet() }
)
mapState.placeType?.let { placeType ->

EventLogger.clickPartnerRestaurant(
college = collegeId,
major = departmentId,
partnerRestaurantId = info.id.toLong()
)

MapRestaurantBottomSheet(
storeName = info.storeName,
placeType = placeType,
mapRestaurantList = mapState.restaurantInfoList,
onDismiss = {
onHidePartnershipSheet()
}
)
}
}
}

Expand All @@ -278,19 +322,23 @@ fun MapFragmentComposeView(
NaverMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
uiSettings = MapUiSettings(isZoomControlEnabled = false, isLocationButtonEnabled = true),
uiSettings = MapUiSettings(
isZoomControlEnabled = false,
isLocationButtonEnabled = true
),
locationSource = locationSource,
contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.bottom_nav_height)),
properties = MapProperties(
locationTrackingMode = LocationTrackingMode.Follow,
),
onLocationChange = { location ->
// 위치가 업데이트되면 위치 권한 있다고 간주
hasLocationPermission = true
},
) {
mapState.partnerships.forEach { partnership ->
val markerState = rememberMarkerState(position = LatLng(partnership.latitude, partnership.longitude))
val markerState = rememberMarkerState(
position = LatLng(
partnership.latitude,
partnership.longitude
)
)

Marker(
icon = OverlayImage.fromResource(
Expand All @@ -309,11 +357,11 @@ fun MapFragmentComposeView(
captionTextSize = 10.sp,
onClick = {
if (partnership.partnershipInfos.isEmpty()) {
// 제휴 정보가 없을 때는 토스트만 띄우고 바텀시트는 안 띄움
Toast.makeText(context, "제휴 정보가 없습니다.", Toast.LENGTH_SHORT).show()
showToast("제휴 정보가 없습니다.")
true
} else {
// 제휴 정보가 있을 때만 바텀시트 띄움
// LaunchedEffect에서 자동으로 표시됨
viewModel.selectPartnershipByStoreName(partnership.storeName)
true
}
Expand All @@ -328,22 +376,8 @@ fun MapFragmentComposeView(
PartnershipFilterToggle(
selected = selectedFilter,
onSelectedChange = { next ->
if (mapState.showPartnershipBottomSheet) return@PartnershipFilterToggle

val hasDepartment = !departmentName.equals("학과")

if (next == FilterType.Mine && !hasDepartment) {
// 전환 막기: selectedFilter는 그대로 (All 유지)
// 학과 입력 바텀시트 띄우기
scope.launch {
// suspend 함수이므로 코루틴 내에서 실행
sheetState.show()
}
return@PartnershipFilterToggle
}

// 학과 정보가 있거나 All 선택은 정상 전환
selectedFilter = next
if (partnershipSheetState.isVisible) return@PartnershipFilterToggle
onSelectedFilterChange(next)
},
modifier = Modifier.padding(top = 12.dp),
departmentName = departmentName.toString()
Expand Down Expand Up @@ -382,6 +416,6 @@ fun Context.findActivityOrNull(): Activity? = when (this) {
@Composable
fun MapFragmentComposeViewPreview() {
EatssuTheme {
MapFragmentComposeView()
MapRoute()
}
}
Loading