Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 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
private fun MapScreen(
Copy link
Member

Choose a reason for hiding this comment

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

p2 저는 internal fun으로 하는 편입니다

  • internal fun : UI
  • private fun: 내부 컴포넌트

요즘 요렇게 많이 쓰이는 것 같아요. 비슷한 접근제어자라서 제 견해로는 UI 전체인지, 컴포넌트인지를 구분하는 용도로 써도 좋을 것 같아요

Copy link
Member

Choose a reason for hiding this comment

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

저는 MapScreen / internal fun MapScreen 이렇게 하는 편인데,

전에 유리님께서 route 네이밍을 얘기하셨어서, MapRoute / internal MapScreen 요렇게 절충안을 제안해봅니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

internal은 모듈 내에서 접근 가능, private은 파일 내 접근 가능으로 알고 있었습니다

리뷰를 읽고 사실 굳이 internal을 써야 할까 의문이 들었었는데,
곰곰쓰 생각해보니까

하나의 compose 파일 내에 모든 screen이 존재하라는 보장이 없으니(길어지면 보통 분리하니까! 또는 Screen이 여러개라면 별도 파일로 둘 수도 있고) private보다는 internal이 맞겠네요!
내부 컴포넌트는 core컴포넌트가 아닌 이상 보통 한 파일에 두니까 private이 맞을 것 같구요

이부분도 wiki에 적어두면 좋을 거 같아요
그런데 이렇게 정한 이유가 위에 제가 설명한 내용이 맞는지 검토해주시고 동의해주시면 wiki에 적어두겠습니다!

우선 해당 부분 코드는 수정했습니다!

Copy link
Member

Choose a reason for hiding this comment

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

맞습니다~
근데 또 생각해보니까 내부컴포넌트도 별도의 파일로 분리하고 있지 않나? 라는 생각이 듭니다 🤔

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