diff --git a/app/src/debug/res/drawable/draw_require_coure_name_border.xml b/app/src/debug/res/drawable/draw_require_coure_name_border.xml new file mode 100644 index 000000000..b85d056a6 --- /dev/null +++ b/app/src/debug/res/drawable/draw_require_coure_name_border.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/debug/res/drawable/ic_current_location.xml b/app/src/debug/res/drawable/ic_current_location.xml new file mode 100644 index 000000000..b46678518 --- /dev/null +++ b/app/src/debug/res/drawable/ic_current_location.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/debug/res/drawable/ic_custom_location.xml b/app/src/debug/res/drawable/ic_custom_location.xml new file mode 100644 index 000000000..ab8e2741b --- /dev/null +++ b/app/src/debug/res/drawable/ic_custom_location.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/debug/res/drawable/ic_drag.xml b/app/src/debug/res/drawable/ic_drag.xml new file mode 100644 index 000000000..ce23336c5 --- /dev/null +++ b/app/src/debug/res/drawable/ic_drag.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/debug/res/drawable/ic_info_window.xml b/app/src/debug/res/drawable/ic_info_window.xml new file mode 100644 index 000000000..742a04813 --- /dev/null +++ b/app/src/debug/res/drawable/ic_info_window.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/java/com/runnect/runnect/data/dto/LocationData.kt b/app/src/main/java/com/runnect/runnect/data/dto/LocationData.kt new file mode 100644 index 000000000..95b566cce --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/data/dto/LocationData.kt @@ -0,0 +1,11 @@ +package com.runnect.runnect.data.dto + + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class LocationData( + val buildingName: String, + val fullAddress: String, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/data/dto/SearchResultEntity.kt b/app/src/main/java/com/runnect/runnect/data/dto/SearchResultEntity.kt index 512f87bbe..76279cc67 100644 --- a/app/src/main/java/com/runnect/runnect/data/dto/SearchResultEntity.kt +++ b/app/src/main/java/com/runnect/runnect/data/dto/SearchResultEntity.kt @@ -8,5 +8,6 @@ import kotlinx.android.parcel.Parcelize data class SearchResultEntity( val fullAddress: String, val name: String, - val locationLatLng: LatLng, + val locationLatLng: LatLng?, + val mode: String ) : Parcelable diff --git a/app/src/main/java/com/runnect/runnect/data/dto/tmap/geocoding/ResponseReverseGeocodingDto.kt b/app/src/main/java/com/runnect/runnect/data/dto/tmap/geocoding/ResponseReverseGeocodingDto.kt new file mode 100644 index 000000000..928e1d5f8 --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/data/dto/tmap/geocoding/ResponseReverseGeocodingDto.kt @@ -0,0 +1,94 @@ +package com.runnect.runnect.data.dto.tmap.geocoding + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseReverseGeocodingDto( + @SerialName("addressInfo") + val addressInfo: AddressInfo +) { + @Serializable + data class AddressInfo( + @SerialName("addressKey") + val addressKey: String? = null, + @SerialName("addressType") + val addressType: String, + @SerialName("adminDong") + val adminDong: String, + @SerialName("adminDongCode") + val adminDongCode: String, + @SerialName("adminDongCoord") + val adminDongCoord: AdminDongCoord? = null, + @SerialName("buildingIndex") + val buildingIndex: String, + @SerialName("buildingName") + val buildingName: String, + @SerialName("bunji") + val bunji: String, + @SerialName("city_do") + val cityDo: String, + @SerialName("eup_myun") + val eupMyun: String, + @SerialName("fullAddress") + val fullAddress: String, + @SerialName("gu_gun") + val guGun: String, + @SerialName("legalDong") + val legalDong: String, + @SerialName("legalDongCode") + val legalDongCode: String, + @SerialName("legalDongCoord") + val legalDongCoord: LegalDongCoord? = null, + @SerialName("mappingDistance") + val mappingDistance: String, + @SerialName("ri") + val ri: String, + @SerialName("roadAddressKey") + val roadAddressKey: String? = null, + @SerialName("roadCode") + val roadCode: String, + @SerialName("roadCoord") + val roadCoord: RoadCoord? = null, + @SerialName("roadName") + val roadName: String + ) { + + @Serializable + data class AdminDongCoord( + @SerialName("lat") + val lat: String, + @SerialName("latEntr") + val latEntr: String, + @SerialName("lon") + val lon: String, + @SerialName("lonEntr") + val lonEntr: String + ) + + @Serializable + data class RoadCoord( + @SerialName("lat") + val lat: String, + @SerialName("latEntr") + val latEntr: String, + @SerialName("lon") + val lon: String, + @SerialName("lonEntr") + val lonEntr: String + ) + + @Serializable + data class LegalDongCoord( + @SerialName("lat") + val lat: String, + @SerialName("latEntr") + val latEntr: String, + @SerialName("lon") + val lon: String, + @SerialName("lonEntr") + val lonEntr: String + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/data/repository/DepartureSearchRepositoryImpl.kt b/app/src/main/java/com/runnect/runnect/data/repository/DepartureSearchRepositoryImpl.kt index d24027cb4..b96ab33f5 100644 --- a/app/src/main/java/com/runnect/runnect/data/repository/DepartureSearchRepositoryImpl.kt +++ b/app/src/main/java/com/runnect/runnect/data/repository/DepartureSearchRepositoryImpl.kt @@ -24,7 +24,8 @@ class DepartureSearchRepositoryImpl @Inject constructor(private val departureSou SearchResultEntity( fullAddress = makeMainAdress(it), name = it.name ?: "", - locationLatLng = LatLng(it.noorLat.toDouble(), it.noorLon.toDouble()) + locationLatLng = LatLng(it.noorLat.toDouble(), it.noorLon.toDouble()), + mode = "searchLocation" //현위치, 지도에서 출발과 구분하기 위한 식별자 ) } return changedData diff --git a/app/src/main/java/com/runnect/runnect/data/repository/ReverseGeocodingRepositoryImpl.kt b/app/src/main/java/com/runnect/runnect/data/repository/ReverseGeocodingRepositoryImpl.kt new file mode 100644 index 000000000..8fd425c0a --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/data/repository/ReverseGeocodingRepositoryImpl.kt @@ -0,0 +1,21 @@ +package com.runnect.runnect.data.repository + +import com.runnect.runnect.data.dto.LocationData +import com.runnect.runnect.data.source.remote.RemoteReverseGeocodingDataSource +import com.runnect.runnect.domain.ReverseGeocodingRepository +import javax.inject.Inject + +class ReverseGeocodingRepositoryImpl @Inject constructor(private val reverseGeocodingDataSource: RemoteReverseGeocodingDataSource) : + ReverseGeocodingRepository { + override suspend fun getLocationInfoUsingLatLng( + lat: Double, + lon: Double + ): LocationData { + val response = + reverseGeocodingDataSource.getLocationInfoUsingLatLng(lat = lat, lon = lon).body() + return LocationData( + buildingName = response?.addressInfo?.buildingName ?: "buildingName fail", + fullAddress = response?.addressInfo?.fullAddress ?: "fullAddress fail" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/data/service/ReverseGeocodingService.kt b/app/src/main/java/com/runnect/runnect/data/service/ReverseGeocodingService.kt new file mode 100644 index 000000000..706c3a77a --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/data/service/ReverseGeocodingService.kt @@ -0,0 +1,23 @@ +package com.runnect.runnect.data.service + +import com.runnect.runnect.BuildConfig +import com.runnect.runnect.data.dto.tmap.SearchResponseTmapDto +import com.runnect.runnect.data.dto.tmap.geocoding.ResponseReverseGeocodingDto +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface ReverseGeocodingService { + + @GET("/tmap/geo/reversegeocoding?") + suspend fun getLocationUsingLatLng( + @Header("appKey") appKey: String = BuildConfig.TMAP_API_KEY, + @Query("version") version: Int = 1, + @Query("callback") callback: String? = null, + @Query("lat") lat: Double, + @Query("lon") lon: Double, + @Query("coordType") coordType: String? = "WGS84GEO", + @Query("addressType") addresstType: String? = "A04", + ): Response +} diff --git a/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteReverseGeocodingDataSource.kt b/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteReverseGeocodingDataSource.kt new file mode 100644 index 000000000..a4f29514a --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/data/source/remote/RemoteReverseGeocodingDataSource.kt @@ -0,0 +1,14 @@ +package com.runnect.runnect.data.source.remote + +import com.runnect.runnect.data.dto.tmap.geocoding.ResponseReverseGeocodingDto +import com.runnect.runnect.data.service.ReverseGeocodingService +import retrofit2.Response +import javax.inject.Inject + +class RemoteReverseGeocodingDataSource @Inject constructor(private val reverseGeocodingService: ReverseGeocodingService) { + suspend fun getLocationInfoUsingLatLng( + lat: Double, + lon: Double + ): Response = + reverseGeocodingService.getLocationUsingLatLng(lat = lat, lon = lon) +} \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/di/RepositoryModule.kt b/app/src/main/java/com/runnect/runnect/di/RepositoryModule.kt index 7511b7be2..f45ad8e50 100644 --- a/app/src/main/java/com/runnect/runnect/di/RepositoryModule.kt +++ b/app/src/main/java/com/runnect/runnect/di/RepositoryModule.kt @@ -1,12 +1,11 @@ package com.runnect.runnect.di -import com.runnect.runnect.data.service.* import com.runnect.runnect.data.repository.* +import com.runnect.runnect.data.service.* import com.runnect.runnect.data.source.remote.* import com.runnect.runnect.domain.* import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -34,6 +33,10 @@ interface RepositoryModule { @Binds fun bindLoginRepository(loginRepositoryImpl: LoginRepositoryImpl): LoginRepository + @Singleton + @Binds + fun bindReverseGeocodingRepository(reverseGeocodingRepositoryImpl: ReverseGeocodingRepositoryImpl): ReverseGeocodingRepository + @Singleton @Binds fun bindBannerRepository(bannerRepositoryImpl: BannerRepositoryImpl): BannerRepository diff --git a/app/src/main/java/com/runnect/runnect/di/ServiceModule.kt b/app/src/main/java/com/runnect/runnect/di/ServiceModule.kt index d302d0fa1..bcee7268b 100644 --- a/app/src/main/java/com/runnect/runnect/di/ServiceModule.kt +++ b/app/src/main/java/com/runnect/runnect/di/ServiceModule.kt @@ -36,6 +36,11 @@ object ServiceModule { fun provideKSearchService(@RetrofitModule.Tmap tmapRetrofit: Retrofit) = tmapRetrofit.create(SearchService::class.java) + @Singleton + @Provides + fun provideReverseGeocodingService(@RetrofitModule.Tmap tmapRetrofit: Retrofit) = + tmapRetrofit.create(ReverseGeocodingService::class.java) + @Singleton @Provides fun provideBannerService() = Firebase.firestore diff --git a/app/src/main/java/com/runnect/runnect/domain/ReverseGeocodingRepository.kt b/app/src/main/java/com/runnect/runnect/domain/ReverseGeocodingRepository.kt new file mode 100644 index 000000000..ee968dbac --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/domain/ReverseGeocodingRepository.kt @@ -0,0 +1,7 @@ +package com.runnect.runnect.domain + +import com.runnect.runnect.data.dto.LocationData + +interface ReverseGeocodingRepository { + suspend fun getLocationInfoUsingLatLng(lat: Double, lon: Double): LocationData +} \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/presentation/draw/DrawActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/draw/DrawActivity.kt index ca4d331d5..1fa25bfe5 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/draw/DrawActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/draw/DrawActivity.kt @@ -1,25 +1,34 @@ package com.runnect.runnect.presentation.draw -import android.app.AlertDialog -import android.content.ContentValues import android.content.Intent import android.graphics.Bitmap import android.graphics.Color import android.graphics.PointF import android.net.Uri import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.Toast import androidx.activity.viewModels import androidx.core.content.FileProvider import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.android.material.bottomsheet.BottomSheetDialog import com.naver.maps.geometry.LatLng import com.naver.maps.geometry.LatLngBounds +import com.naver.maps.map.CameraAnimation import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.LocationTrackingMode import com.naver.maps.map.MapFragment import com.naver.maps.map.NaverMap import com.naver.maps.map.OnMapReadyCallback +import com.naver.maps.map.overlay.InfoWindow import com.naver.maps.map.overlay.Marker import com.naver.maps.map.overlay.OverlayImage import com.naver.maps.map.overlay.PathOverlay @@ -30,6 +39,7 @@ import com.runnect.runnect.data.dto.CourseData import com.runnect.runnect.data.dto.SearchResultEntity import com.runnect.runnect.data.dto.UploadLatLng import com.runnect.runnect.databinding.ActivityDrawBinding +import com.runnect.runnect.databinding.BottomsheetRequireCourseNameBinding import com.runnect.runnect.presentation.MainActivity import com.runnect.runnect.presentation.countdown.CountDownActivity import com.runnect.runnect.presentation.login.LoginActivity @@ -41,6 +51,8 @@ import kotlinx.android.synthetic.main.custom_dialog_make_course.view.btn_run import kotlinx.android.synthetic.main.custom_dialog_make_course.view.btn_storage import kotlinx.android.synthetic.main.custom_dialog_require_login.view.btn_cancel import kotlinx.android.synthetic.main.custom_dialog_require_login.view.btn_login +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.File @@ -52,8 +64,15 @@ import java.math.RoundingMode class DrawActivity : com.runnect.runnect.binding.BindingActivity(R.layout.activity_draw), OnMapReadyCallback { - private lateinit var naverMap: NaverMap private lateinit var locationSource: FusedLocationSource + private lateinit var currentLocation: LatLng + private lateinit var fusedLocation: FusedLocationProviderClient//현재 위치 반환 객체 변수) + + var isCustomLocationMode: Boolean = false + var isSearchLocationMode: Boolean = false + var isCurrentLocationMode: Boolean = false + + private lateinit var naverMap: NaverMap private lateinit var departureLatLng: LatLng private lateinit var animDown: Animation private lateinit var animUp: Animation @@ -71,40 +90,37 @@ class DrawActivity : private var isMarkerAvailable: Boolean = false var isVisitorMode: Boolean = MainActivity.isVisitorMode + var isFirstInit: Boolean = true + + private lateinit var bottomSheetBinding: BottomsheetRequireCourseNameBinding // Bottom Sheet 바인딩 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding.model = viewModel binding.lifecycleOwner = this - if (::searchResult.isInitialized.not()) { - intent?.let { - searchResult = - intent.getParcelableExtra(EXTRA_SEARCH_RESULT) ?: throw Exception("데이터가 존재하지 않습니다.") - - Timber.tag(ContentValues.TAG).d("searchResult : $searchResult") - viewModel.searchResult.value = searchResult - initView() - courseFinish() - addObserver() - backButton() - activateDrawCourse() - } - } + initMapView() + getSearchIntent() + addObserver() + backButton() + courseFinish() } - override fun onBackPressed() { - finish() - overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right) + private fun getSearchIntent() { + searchResult = + intent.getParcelableExtra(EXTRA_SEARCH_RESULT) ?: throw Exception("데이터가 존재하지 않습니다.") } - private fun initView() { + private fun initMapView() { + fusedLocation = LocationServices.getFusedLocationProviderClient(this) // 현위치 정보 제공 + val fm = supportFragmentManager val mapFragment = fm.findFragmentById(R.id.mapView) as MapFragment? ?: MapFragment.newInstance().also { fm.beginTransaction().add(R.id.mapView, it).commit() } mapFragment.getMapAsync(this) + locationSource = FusedLocationSource( this, LOCATION_PERMISSION_REQUEST_CODE ) @@ -112,28 +128,188 @@ class DrawActivity : override fun onMapReady(map: NaverMap) { naverMap = map - naverMap.maxZoom = 18.0 - naverMap.minZoom = 10.0 + initMode() + setLocationTrackingMode() + setCurrentLocationIcon() + setZoomControl() + setLocationChangedListener() + setCameraFinishedListener() + } + private fun initMode() { + when (searchResult.mode) { + "searchLocation" -> initSearchLocationMode() + "currentLocation" -> initCurrentLocationMode() + "customLocation" -> initCustomLocationMode() + } + } + private fun initSearchLocationMode() { + isSearchLocationMode = true - //네이버 맵 sdk에 위치 정보 제공 - locationSource = FusedLocationSource(this@DrawActivity, LOCATION_PERMISSION_REQUEST_CODE) - naverMap.locationSource = locationSource + with(binding){ + tvGuide.isVisible = false + btnDraw.text = CREATE_COURSE + } + + viewModel.searchResult.value = searchResult + viewModel.departureName.value = + searchResult.name //searchLocationMode일 땐 departureName 값 세팅해주는 부분이 따로 없어서 여기에 작성해놓음 + + setDepartureLatLng( + latLng = LatLng( + searchResult.locationLatLng!!.latitude, searchResult.locationLatLng!!.longitude + ) + ) + activateDrawCourse() - drawCourse() + lifecycleScope.launch { + delay(500) //인위적으로 늦춰줌 + if (::departureLatLng.isInitialized) { + drawCourse(departureLatLng = departureLatLng) + } + } + } + private fun initCurrentLocationMode() { + isCurrentLocationMode = true + isMarkerAvailable = true + + with(binding) { + customDepartureMarker.isVisible = false + tvGuide.isVisible = false + } + showDrawGuide() + hideDeparture() + showDrawCourse() + } + private fun initCustomLocationMode() { + isCustomLocationMode = true + + with(binding) { + customDepartureMarker.isVisible = true + customDepartureInfoWindow.isVisible = true + tvCustomDepartureGuideFrame.isVisible = true + + btnPreStart.setOnClickListener { + isMarkerAvailable = true + showDrawGuide() + hideDeparture() + showDrawCourse() + drawCourse(departureLatLng = getCenterPosition()) + hideFloatedDeparture() + } + } + } + + private fun hideFloatedDeparture() { + with(binding) { + customDepartureMarker.isVisible = false + customDepartureInfoWindow.isVisible = false + } + } + + private fun setDepartureLatLng(latLng: LatLng) { + departureLatLng = LatLng( + latLng.latitude, latLng.longitude + ) + } + + private fun getLocationInfoUsingLatLng(lat: Double, lon: Double) { + viewModel.getLocationInfoUsingLatLng(lat = lat, lon = lon) + } + + fun setZoomControl() { + naverMap.maxZoom = 18.0 + naverMap.minZoom = 10.0 val uiSettings = naverMap.uiSettings uiSettings.isZoomControlEnabled = false } + fun setCurrentLocationIcon() { + val locationOverlay = naverMap.locationOverlay + locationOverlay.icon = OverlayImage.fromResource(R.drawable.current_location) + } + + private fun setCameraFinishedListener() { + naverMap.addOnCameraIdleListener { + val centerLatLng = getCenterPosition() + if (::currentLocation.isInitialized) { + getLocationInfoUsingLatLng( //코스를 다 그린 후에도 계속 통신이 돌아서 리소스 낭비를 막기 위한 조치 필요 + lat = centerLatLng.latitude, lon = centerLatLng.longitude + ) + } + Timber.tag("카메라-끝").d("$centerLatLng") //위에 통신이 비동기로 돌아서 이게 먼저 찍힘. + } + } + + fun getCenterPosition(): LatLng { + val cameraPosition = naverMap.cameraPosition + return cameraPosition.target // 중심 좌표 + } + private fun setLocationChangedListener() { + naverMap.addOnLocationChangeListener { location -> + currentLocation = LatLng(location.latitude, location.longitude) + + naverMap.locationOverlay.position = currentLocation + naverMap.locationOverlay.isVisible = false + setDepartureLatLng(latLng = LatLng(currentLocation.latitude, currentLocation.longitude)) + + //같은 scope 안에 넣었으니 setDepartureLatLng 다음에 drawCourse가 실행되는 것이 보장됨 + //이때 isFirstInit의 초기값을 true로 줘서 최초 1회는 실행되게 하고 이후 drawCourse 내에서 isFirstInit 값을 false로 바꿔줌 + //뒤의 조건을 안 달아주면 다른 mode에서는 버튼을 클릭하기도 전에 drawCourse()가 돌 거라 안 됨. + if(isFirstInit && isCurrentLocationMode){ + drawCourse(departureLatLng = departureLatLng) + } + } + } + + private fun setLocationTrackingMode() { + naverMap.locationSource = locationSource + + if (isCurrentLocationMode || isCustomLocationMode) { + naverMap.locationTrackingMode = + LocationTrackingMode.Follow //위치추적 모드 Follow - 자동으로 camera 이동 + } + } + private fun courseFinish() { binding.btnDraw.setOnClickListener { - if (isVisitorMode) { requireVisitorLogin() + return@setOnClickListener + } + when { + isSearchLocationMode -> createMbr() + isCurrentLocationMode || isCustomLocationMode -> requireCourseNameDialog().show() + } + } + } + + private fun requireCourseNameDialog(): BottomSheetDialog { + bottomSheetBinding = BottomsheetRequireCourseNameBinding.inflate(layoutInflater) + val bottomSheetView = bottomSheetBinding.root + val etCourseName = bottomSheetBinding.etCourseName + val btnCreateCourse = bottomSheetBinding.btnCreateCourse + + etCourseName.addTextChangedListener { + if (!etCourseName.text.isNullOrEmpty()) { + btnCreateCourse.setBackgroundResource(R.drawable.radius_10_m1_button) + btnCreateCourse.isEnabled = true + viewModel.departureName.value = etCourseName.text.toString() } else { - createMbr() + btnCreateCourse.setBackgroundResource(R.drawable.radius_10_g3_button) + btnCreateCourse.isEnabled = false } + Timber.tag("EditTextValue").d("${viewModel.departureName.value}") } + + btnCreateCourse.setOnClickListener { + createMbr() + } + + val bottomSheetDialog = BottomSheetDialog(this) + bottomSheetDialog.setContentView(bottomSheetView) + + return bottomSheetDialog } private fun activateDrawCourse() { @@ -173,6 +349,8 @@ class DrawActivity : tvDeparture.startAnimation(animUp) viewTopFrame.isVisible = false tvDeparture.isVisible = false + tvGuide.isVisible = false + tvCustomDepartureGuideFrame.isVisible = false //Bottom invisible viewBottomSheetFrame.startAnimation(animDown) @@ -190,6 +368,17 @@ class DrawActivity : private fun addObserver() { observeIsBtnAvailable() observeDrawState() + + viewModel.reverseGeocodingResult.observe(this) { + val buildingName = viewModel.reverseGeocodingResult.value!!.buildingName + + if (buildingName.isEmpty()) { + binding.tvPlaceName.text = CUSTOM_DEPARTURE + } else binding.tvPlaceName.text = buildingName + + binding.tvPlaceAddress.text = + viewModel.reverseGeocodingResult.value?.fullAddress ?: "fail" + } } private fun activateMarkerBackBtn() { @@ -229,7 +418,7 @@ class DrawActivity : binding.indeterminateBar.isVisible = false } - private fun observeDrawState() { + private fun observeDrawState() { //분기 처리를 더 해줘야 함. 서버에서 400 날아오는데 이게 success로 빠져서 '코스 생성 완료' 팝업이 뜨고 있음. viewModel.drawState.observe(this) { when (it) { UiState.Empty -> hideLoadingBar() @@ -263,7 +452,7 @@ class DrawActivity : publicCourseId = null, touchList = touchList, startLatLng = departureLatLng, - departure = searchResult.name, + departure = viewModel.departureName.value!!, distance = viewModel.distanceSum.value!!, image = captureUri.toString(), dataFrom = "fromDrawCourse" @@ -315,56 +504,63 @@ class DrawActivity : } } - - //카메라 위치 변경 함수 private fun cameraUpdate(location: Any) { - //이건 맨 처음 지도 켤 때 startLocation으로 위치 옮길 때 사용 + //맨 처음 지도 켤 때 startLocation으로 위치 옮길 때 사용 if (location is LatLng) { - val cameraUpdate = CameraUpdate.scrollTo(location) + val cameraUpdate = CameraUpdate.scrollTo(location).animate(CameraAnimation.Easing) naverMap.moveCamera(cameraUpdate) } - //이건 카메라 이동해서 캡쳐할 때 사용 + //카메라 이동해서 캡쳐할 때 사용 else if (location is LatLngBounds) { val cameraUpdate = CameraUpdate.fitBounds(location) naverMap.moveCamera(cameraUpdate) } } - private fun drawCourse() { - createDepartureMarker() + private fun drawCourse(departureLatLng: LatLng) { + isFirstInit = false + createDepartureMarker(departureLatLng = departureLatLng) createRouteMarker() deleteRouteMarker() } - private fun createDepartureMarker() { - setDepartureLatLng() - setDepartureMarker() - addDepartureToCoords() - addDepartureToCalcDistanceList() + private fun createDepartureMarker(departureLatLng: LatLng) { + setDepartureMarker(departureLatLng = departureLatLng) + addDepartureToCoords(departureLatLng = departureLatLng) + addDepartureToCalcDistanceList(departureLatLng = departureLatLng) } - private fun setDepartureLatLng() { - departureLatLng = LatLng( - searchResult.locationLatLng.latitude, searchResult.locationLatLng.longitude - ) - } - - private fun setDepartureMarker() { + private fun setDepartureMarker(departureLatLng: LatLng) { val departureMarker = Marker() departureMarker.position = LatLng(departureLatLng.latitude, departureLatLng.longitude) - departureMarker.anchor = PointF(0.5f, 0.7f) - departureMarker.icon = OverlayImage.fromResource(R.drawable.marker_departure) + departureMarker.anchor = PointF(0.5f, 0.5f) + departureMarker.icon = OverlayImage.fromResource(R.drawable.runnect_marker) departureMarker.map = naverMap - cameraUpdate( - LatLng(departureLatLng.latitude, departureLatLng.longitude) - ) + + if (isSearchLocationMode) { + cameraUpdate( + LatLng(departureLatLng.latitude, departureLatLng.longitude) + ) // 현위치에서 출발할 때 이것 때문에 트랙킹 모드 활성화 시 카메라 이동하는 게 묻혔음 + } + setCustomInfoWindow(marker = departureMarker) + } + + private fun setCustomInfoWindow(marker: Marker) { + val infoWindow = InfoWindow() + infoWindow.adapter = object : InfoWindow.ViewAdapter() { + override fun getView(p0: InfoWindow): View { + return LayoutInflater.from(this@DrawActivity) + .inflate(R.layout.custom_info_window, binding.root as ViewGroup, false) + } + } + infoWindow.open(marker) } - private fun addDepartureToCoords() { + private fun addDepartureToCoords(departureLatLng: LatLng) { coords.add(LatLng(departureLatLng.latitude, departureLatLng.longitude)) } - private fun addDepartureToCalcDistanceList() { + private fun addDepartureToCalcDistanceList(departureLatLng: LatLng) { calcDistanceList.add( LatLng( departureLatLng.latitude, departureLatLng.longitude @@ -374,16 +570,17 @@ class DrawActivity : private fun createRouteMarker() { naverMap.setOnMapClickListener { _, coord -> - if (isMarkerAvailable) { - viewModel.isBtnAvailable.value = true - if (touchList.size < MAX_MARKER_NUM) { - addCoordsToTouchList(coord) - setRouteMarker(coord) - generateRouteLine(coord) - calcDistance() - } else { - Toast.makeText(this, "마커는 20개까지 생성 가능합니다", Toast.LENGTH_SHORT).show() - } + if (!isMarkerAvailable) return@setOnMapClickListener + + viewModel.isBtnAvailable.value = true + + if (touchList.size < MAX_MARKER_NUM) { + addCoordsToTouchList(coord) + setRouteMarker(coord) + generateRouteLine(coord) + calcDistance() + } else { + Toast.makeText(this, NOTIFY_LIMIT_MARKER_NUM, Toast.LENGTH_SHORT).show() } } } @@ -461,7 +658,6 @@ class DrawActivity : } private fun calcDistance() { - for (i in 0 until touchList.size) { if (!calcDistanceList.contains(touchList[i])) { calcDistanceList.add(touchList[i]) @@ -512,14 +708,24 @@ class DrawActivity : UploadLatLng(latLng.latitude, latLng.longitude) } viewModel.path.value = uploadLatLngList - viewModel.departureAddress.value = searchResult.fullAddress - viewModel.departureName.value = searchResult.name + + when { + isSearchLocationMode -> { + viewModel.departureAddress.value = searchResult.fullAddress + viewModel.departureName.value = searchResult.name + } + + isCurrentLocationMode || isCustomLocationMode -> { + viewModel.departureAddress.value = + viewModel.reverseGeocodingResult.value?.fullAddress + } + } + } // Get uri of images from camera function private fun getImageUri(inImage: Bitmap): Uri { - - val tempFile = File.createTempFile("temprentpk", ".png") + val tempFile = File.createTempFile("CreatedCourse", ".png") val bytes = ByteArrayOutputStream() inImage.compress(Bitmap.CompressFormat.PNG, 100, bytes) val bitmapData = bytes.toByteArray() @@ -535,6 +741,11 @@ class DrawActivity : return uri } + override fun onBackPressed() { + finish() + overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right) + } + companion object { private const val LOCATION_PERMISSION_REQUEST_CODE = 1000 const val MAX_MARKER_NUM = 20 @@ -543,5 +754,8 @@ class DrawActivity : const val EXTRA_SEARCH_RESULT = "searchResult" const val EXTRA_COURSE_DATA = "CourseData" const val EXTRA_FRAGMENT_REPLACEMENT_DIRECTION = "fragmentReplacementDirection" + const val CUSTOM_DEPARTURE = "내가 설정한 출발지" + const val NOTIFY_LIMIT_MARKER_NUM = "마커는 20개까지 생성 가능합니다" + const val CREATE_COURSE = "완성하기" } } \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/presentation/draw/DrawViewModel.kt b/app/src/main/java/com/runnect/runnect/presentation/draw/DrawViewModel.kt index 905d88c63..8149dc892 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/draw/DrawViewModel.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/draw/DrawViewModel.kt @@ -5,10 +5,12 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.runnect.runnect.data.dto.LocationData import com.runnect.runnect.data.dto.SearchResultEntity import com.runnect.runnect.data.dto.UploadLatLng import com.runnect.runnect.data.dto.response.ResponsePostCourseDto import com.runnect.runnect.domain.CourseRepository +import com.runnect.runnect.domain.ReverseGeocodingRepository import com.runnect.runnect.presentation.state.UiState import com.runnect.runnect.util.ContentUriRequestBody import dagger.hilt.android.lifecycle.HiltViewModel @@ -23,7 +25,12 @@ import timber.log.Timber import javax.inject.Inject @HiltViewModel -class DrawViewModel @Inject constructor(val courseRepository: CourseRepository) : ViewModel() { +class DrawViewModel @Inject constructor( + val courseRepository: CourseRepository, + val reverseGeocodingRepository: ReverseGeocodingRepository +) : ViewModel() { + + val editTextValue = MutableLiveData() private var _drawState = MutableLiveData(UiState.Empty) val drawState: LiveData @@ -37,6 +44,8 @@ class DrawViewModel @Inject constructor(val courseRepository: CourseRepository) val departureName = MutableLiveData() val isBtnAvailable = MutableLiveData(false) + val reverseGeocodingResult = MutableLiveData() + private val _image = MutableLiveData() val image: LiveData @@ -90,7 +99,7 @@ class DrawViewModel @Inject constructor(val courseRepository: CourseRepository) data = RequestBody( path = path.value!!, distance = distanceSum.value!!, - departureAddress = departureAddress.value!!, + departureAddress = departureAddress.value!!, //커스텀의 경우 지금 여기에 들어가는 게 아무것도 없음. departureName = departureName.value!! ) ) @@ -106,6 +115,22 @@ class DrawViewModel @Inject constructor(val courseRepository: CourseRepository) } } + fun getLocationInfoUsingLatLng(lat: Double, lon: Double) { + viewModelScope.launch { + runCatching { + reverseGeocodingRepository.getLocationInfoUsingLatLng( + lat = lat, lon = lon + ) + }.onSuccess { + Timber.tag(ContentValues.TAG).d("통신success") + reverseGeocodingResult.value = it + }.onFailure { + Timber.tag(ContentValues.TAG).d("통신failure : ${it}") + errorMessage.value = it.message + } + } + } + private fun RequestBody( path: List, distance: Float, diff --git a/app/src/main/java/com/runnect/runnect/presentation/search/SearchActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/search/SearchActivity.kt index a4dd3f7ae..5282a2878 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/search/SearchActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/search/SearchActivity.kt @@ -85,12 +85,11 @@ class SearchActivity : startActivity( Intent(this, DrawActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - putExtra(EXTRA_SEARCH_RESULT, item) + putExtra(EXTRA_SEARCH_RESULT, item) //mode == "searchLocation" } ) } - private fun showEmptyView() { with(binding) { ivNoSearchResult.isVisible = true @@ -179,6 +178,38 @@ class SearchActivity : return false } }) + + binding.cvStartCurrentLocation.setOnClickListener { + startCurrentLocation() + } + + binding.cvStartCustomLocation.setOnClickListener { + startCustomLocation() + } + } + + fun startCurrentLocation() { + startActivity( + Intent(this, DrawActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + putExtra( + EXTRA_SEARCH_RESULT, + SearchResultEntity(fullAddress = "", name = "", locationLatLng = null, mode = "currentLocation") + ) + } + ) + } + + fun startCustomLocation() { + startActivity( + Intent(this, DrawActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + putExtra( + EXTRA_SEARCH_RESULT, + SearchResultEntity(fullAddress = "", name = "", locationLatLng = null, mode = "customLocation") + ) + } + ) } //키보드 밖 터치 시, 키보드 내림 diff --git a/app/src/main/java/com/runnect/runnect/util/extension/ContextExt.kt b/app/src/main/java/com/runnect/runnect/util/extension/ContextExt.kt index 1b961fec6..5b503855e 100644 --- a/app/src/main/java/com/runnect/runnect/util/extension/ContextExt.kt +++ b/app/src/main/java/com/runnect/runnect/util/extension/ContextExt.kt @@ -11,6 +11,7 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.LinearLayoutCompat import androidx.fragment.app.Fragment import com.google.android.material.bottomsheet.BottomSheetDialog @@ -133,6 +134,11 @@ fun BottomSheetDialog.setEditBottomSheetClickListener(listener: (which: LinearLa } } +fun BottomSheetDialog.handleEditTextValue(){ + this.setOnShowListener { + val editText = this.layout_edit_frame + } +} fun Context.getStampResId( stampId: String?, diff --git a/app/src/main/res/drawable/draw_custom_departure_tv_guide.xml b/app/src/main/res/drawable/draw_custom_departure_tv_guide.xml new file mode 100644 index 000000000..954cbfe81 --- /dev/null +++ b/app/src/main/res/drawable/draw_custom_departure_tv_guide.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_draw.xml b/app/src/main/res/layout/activity_draw.xml index 7e6c9ed88..c6903bc28 100644 --- a/app/src/main/res/layout/activity_draw.xml +++ b/app/src/main/res/layout/activity_draw.xml @@ -15,6 +15,32 @@ android:layout_height="match_parent" tools:context=".views.location_enroll.LocationEnrollFragment"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bottomsheet_require_course_name.xml b/app/src/main/res/layout/bottomsheet_require_course_name.xml new file mode 100644 index 000000000..d09df3281 --- /dev/null +++ b/app/src/main/res/layout/bottomsheet_require_course_name.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_info_window.xml b/app/src/main/res/layout/custom_info_window.xml new file mode 100644 index 000000000..1b13a92f0 --- /dev/null +++ b/app/src/main/res/layout/custom_info_window.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc7fe168e..028311d2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,7 +54,7 @@ km 시작하기 시작하기 - 지역과 키워드 중심으로 검색해보세요 + 출발지를 설정해주세요 검색결과가 없습니다\n검색어를 다시 확인해주세요 수고하셨습니다! 러닝을 완료했어요! 기록 보러 가기 @@ -115,4 +115,7 @@ 닉네임을 입력해주세요 시작하기 장기간 미접속으로 인해 재로그인이 필요합니다. + 지도를 움직여 출발지를 설정해주세요 + 지도에서 선택 + 다음으로 \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d4aee40fa..2b0e7c8ce 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -14,6 +14,8 @@ @color/transparent_00 false + + @style/AppBottomSheetDialogTheme - + @@ -31,4 +33,17 @@ @android:color/transparent + + + + \ No newline at end of file