Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.shinhan.campung.presentation.ui.components

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateOffsetAsState
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
Expand All @@ -18,7 +17,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
Expand All @@ -39,44 +37,33 @@ fun AnimatedMapTooltip(
type: TooltipType,
modifier: Modifier = Modifier
) {
// 부드러운 위치 애니메이션을 위한 애니메이션 상태
val targetOffset = Offset(
x = position.x - 190f, // 툴팁 중앙 정렬을 위한 오프셋
y = position.y - 320f // 마커 위쪽 250px
)

val animatedOffset by animateOffsetAsState(
targetValue = targetOffset,
animationSpec = tween(
durationMillis = 200, // 빠른 응답성을 위해 200ms
easing = LinearOutSlowInEasing // 부드럽고 자연스러운 움직임
),
label = "tooltip_position"
)
// 완전히 즉각적인 반응 - 애니메이션 없음
val offsetX = position.x - 190f
val offsetY = position.y - 320f

Box(
modifier = modifier
.offset {
// 소수점 위치를 그대로 사용하여 부드러운 움직임 구현
// 완전히 즉각적 - 애니메이션 지연 없음
androidx.compose.ui.unit.IntOffset(
x = animatedOffset.x.toInt(),
y = animatedOffset.y.toInt()
x = offsetX.toInt(),
y = offsetY.toInt()
)
}
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(
animationSpec = tween(250, easing = LinearOutSlowInEasing)
animationSpec = tween(100, easing = LinearEasing)
) + scaleIn(
animationSpec = tween(250, easing = LinearOutSlowInEasing),
initialScale = 0.8f // 더 자연스러운 시작 스케일
animationSpec = tween(100, easing = LinearEasing),
initialScale = 0.9f
),
exit = fadeOut(
animationSpec = tween(150)
animationSpec = tween(50)
) + scaleOut(
animationSpec = tween(150),
targetScale = 0.8f
animationSpec = tween(50),
targetScale = 0.9f
)
) {
// 말풍선 모양 툴팁 (Box + 꼬리)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.Gravity
import android.view.View
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -35,6 +36,8 @@ import com.naver.maps.map.NaverMap
import com.naver.maps.map.overlay.Marker
import com.shinhan.campung.presentation.viewmodel.MapViewModel
import com.shinhan.campung.presentation.ui.map.MapClusterManager
import com.shinhan.campung.presentation.ui.components.MyLocationMarker
import com.shinhan.campung.presentation.ui.components.LocationMarkerManager

private val LatLngSaver: Saver<LatLng?, String> = Saver(
save = { loc -> loc?.let { "${it.latitude},${it.longitude}" } ?: "" },
Expand Down Expand Up @@ -96,6 +99,7 @@ fun CampusMapCard(
fused.lastLocation.addOnSuccessListener { loc ->
if (loc != null && myLatLng == null) {
myLatLng = LatLng(loc.latitude, loc.longitude)

}
}
val cts = CancellationTokenSource()
Expand Down Expand Up @@ -140,7 +144,7 @@ fun CampusMapCard(
val pos = myLatLng
if (map != null && pos != null && !movedToMyLocOnce.value) {
map.moveCamera(CameraUpdate.scrollAndZoomTo(pos, 15.0))
map.locationOverlay.isVisible = true
map.locationOverlay.isVisible = false
map.locationOverlay.position = pos
movedToMyLocOnce.value = true

Expand Down Expand Up @@ -180,72 +184,71 @@ fun CampusMapCard(
)
}
} else {
AndroidView(
factory = { mapView },
modifier = Modifier.fillMaxSize(),
update = { mv ->
if (naverMapRef == null) {
mv.getMapAsync { map ->
naverMapRef = map

map.uiSettings.apply {
// 제스처/버튼 모두 끄기 = 화면 고정
isScrollGesturesEnabled = false
isZoomGesturesEnabled = false
isTiltGesturesEnabled = false
isRotateGesturesEnabled = false
isZoomControlEnabled = false
isScaleBarEnabled = false
isCompassEnabled = false
isLocationButtonEnabled = false
isLogoClickEnabled = false
setLogoMargin(-500, -500, 0, 0)
}

val target = myLatLng ?: initialCamera
map.moveCamera(CameraUpdate.scrollAndZoomTo(target, 15.0))

// 카메라 이동 후 스타일 적용
var cameraIdleListener: NaverMap.OnCameraIdleListener? = null
cameraIdleListener = NaverMap.OnCameraIdleListener {
try {
map.setCustomStyleId("258120eb-1ebf-4b29-97cf-21df68e09c5c")
android.util.Log.d("CampusMapCard", "카메라 idle 후 커스텀 스타일 적용 성공")
// 리스너 제거 (한 번만 실행)
cameraIdleListener?.let { map.removeOnCameraIdleListener(it) }
} catch (e: Exception) {
android.util.Log.e("CampusMapCard", "카메라 idle 후 커스텀 스타일 적용 실패", e)
e.printStackTrace()
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { mapView },
modifier = Modifier.fillMaxSize(),
update = { mv ->
if (naverMapRef == null) {
mv.getMapAsync { map ->
naverMapRef = map

map.uiSettings.apply {
// 제스처/버튼 모두 끄기 = 화면 고정
isScrollGesturesEnabled = false
isZoomGesturesEnabled = false
isTiltGesturesEnabled = false
isRotateGesturesEnabled = false
isZoomControlEnabled = false
isScaleBarEnabled = false
isCompassEnabled = false
isLocationButtonEnabled = false
isLogoClickEnabled = false
setLogoMargin(-500, -500, 0, 0)
}
}
map.addOnCameraIdleListener(cameraIdleListener)

if (myLatLng != null && hasPermission) {
map.locationOverlay.isVisible = true
map.locationOverlay.position = myLatLng!!
}

clusterManager = MapClusterManager(context, map).also {
it.setupClustering()
}

mapViewModel.loadMapContents(
latitude = target.latitude,
longitude = target.longitude
)
}
} else {
naverMapRef?.let { map ->
if (myLatLng != null && hasPermission) {
map.locationOverlay.isVisible = true
map.locationOverlay.position = myLatLng!!
} else {

val target = myLatLng ?: initialCamera
map.moveCamera(CameraUpdate.scrollAndZoomTo(target, 15.0))

// 카메라 이동 후 스타일 적용
var cameraIdleListener: NaverMap.OnCameraIdleListener? = null
cameraIdleListener = NaverMap.OnCameraIdleListener {
try {
map.setCustomStyleId("258120eb-1ebf-4b29-97cf-21df68e09c5c")
android.util.Log.d("CampusMapCard", "카메라 idle 후 커스텀 스타일 적용 성공")
// 리스너 제거 (한 번만 실행)
cameraIdleListener?.let { map.removeOnCameraIdleListener(it) }
} catch (e: Exception) {
android.util.Log.e("CampusMapCard", "카메라 idle 후 커스텀 스타일 적용 실패", e)
e.printStackTrace()
}
}
map.addOnCameraIdleListener(cameraIdleListener)

// 기본 location overlay 비활성화
map.locationOverlay.isVisible = false

clusterManager = MapClusterManager(context, map).also {
it.setupClustering()
}

mapViewModel.loadMapContents(
latitude = target.latitude,
longitude = target.longitude
)
}
}
}
}
)
)

// 모듈화된 위치 마커 관리자 사용
LocationMarkerManager(
map = naverMapRef,
userLocation = myLatLng,
hasLocationPermission = hasPermission,
enableLottieAnimation = true
)
}

// 투명 클릭 레이어
Box(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.shinhan.campung.presentation.ui.components

import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import com.naver.maps.map.NaverMap
import com.shinhan.campung.data.model.SharedLocation
import android.util.Log

/**
* 친구 위치공유 통합 관리 컴포넌트
* 데이터 관리, 상태 관리, UI 표시를 통합적으로 처리
*/
@Composable
fun FriendLocationSharingManager(
map: NaverMap?,
config: SharedLocationConfig = SharedLocationConfig(),
onFriendLocationClick: ((SharedLocation) -> Unit)? = null
) {
val context = LocalContext.current
val dataManager = rememberSharedLocationDataManager()

// 상태 수집
val sharedLocations by dataManager.collectSharedLocationsAsState()
val isSharingEnabled by dataManager.collectLocationSharingEnabledAsState()
val selectedFriend by dataManager.collectSelectedFriendAsState()

// 위치공유 컴포넌트 표시
SharedLocationComponent(
map = map,
sharedLocations = sharedLocations,
isVisible = isSharingEnabled,
onMarkerClick = { location ->
dataManager.selectFriend(location)
onFriendLocationClick?.invoke(location)
Log.d("FriendLocationSharingManager", "친구 위치 클릭: ${location.userName}")
}
)

// 디버그 정보 로깅
LaunchedEffect(sharedLocations, isSharingEnabled) {
Log.d("FriendLocationSharingManager",
"상태 업데이트 - 공유활성화: $isSharingEnabled, 위치개수: ${sharedLocations.size}")
}
}

/**
* FullMapScreen에서 사용할 친구 위치공유 컴포넌트
*/
@Composable
fun FullMapFriendLocationManager(
map: NaverMap?,
onFriendClick: ((SharedLocation) -> Unit)? = null
) {
FriendLocationSharingManager(
map = map,
config = SharedLocationConfig(
showUserNames = true,
markerSize = 100,
enableClickEvents = true
),
onFriendLocationClick = onFriendClick
)
}

/**
* CampusMapCard에서 사용할 친구 위치공유 컴포넌트 (더 작은 사이즈)
*/
@Composable
fun CampusMapFriendLocationManager(
map: NaverMap?,
onFriendClick: ((SharedLocation) -> Unit)? = null
) {
FriendLocationSharingManager(
map = map,
config = SharedLocationConfig(
showUserNames = false, // 작은 맵에서는 이름 숨김
markerSize = 80, // CampusMap에서도 크기 증가
captionSize = 10f,
enableClickEvents = true
),
onFriendLocationClick = onFriendClick
)
}



/**
* 위치 간 거리 계산 (미터)
*/
fun calculateDistance(
lat1: Double, lon1: Double,
lat2: Double, lon2: Double
): Double {
val R = 6371000.0 // 지구 반지름 (미터)
val dLat = Math.toRadians(lat2 - lat1)
val dLon = Math.toRadians(lon2 - lon1)
val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}

/**
* 친구가 근처에 있는지 확인
*/
fun isFriendNearby(
myLocation: com.naver.maps.geometry.LatLng,
friendLocation: SharedLocation,
radiusMeters: Double = 100.0
): Boolean {
val distance = calculateDistance(
myLocation.latitude, myLocation.longitude,
friendLocation.latitude, friendLocation.longitude
)
return distance <= radiusMeters
}
Loading