Skip to content

Commit 30b1e58

Browse files
committed
Merge remote-tracking branch 'origin/103-mission-issue' into 97-feature-mission-service
2 parents a7e91a0 + 46b895a commit 30b1e58

File tree

13 files changed

+208
-107
lines changed

13 files changed

+208
-107
lines changed

README.md

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
![cover2](https://user-images.githubusercontent.com/48354989/205480662-4a958899-33a4-406b-87c1-2c16723a43e5.png)
44

5-
# 프로젝트 소개
6-
7-
![plzstop_icon](https://user-images.githubusercontent.com/48354989/205480621-38dd340e-fbbb-4644-a673-386b241076bc.png) 버스 🚌, 지하철 🚋 멈춰! ⛔ - 대중교통 막차 알람 서비스 **Plz Stop**
8-
5+
## 버스 🚌, 지하철 🚋 멈춰! ⛔
96

107
> 혹시 **막차 시간** 확인을 위해<br/>
118
계속 핸드폰만 붙잡고 계시진 않으신가요? <br/>
@@ -23,21 +20,110 @@
2320

2421
사용자의 현재 위치와 막차시간을 실시간으로 보여주며 사용자가 이동해야할 경로를 안내합니다
2522

23+
### [시연 영상](https://www.youtube.com/watch?v=U1i6KJxiKF0)
2624

27-
28-
29-
# 주요 기능
30-
31-
|**화면 이미지**|**기능**|**설명**|
32-
|:---:|:---:|:--|
33-
|<img width=200 src="https://user-images.githubusercontent.com/48354989/205481015-7fdbfa1b-9dbe-4c3a-ba46-4e155e4f4178.png">|지도|- 사용자의 현재 위치를 **실시간으로 트래킹**하여 보여줍니다.<br/>- 지도상의 임의의 지점을 클릭하면 **현 위치로부터의 거리를 포함한 상세 정보**를 보여줍니다.<br/>- 막차 알람이 설정되어 있으면 화면 하단에 **알람 정보**를 보여줍니다.|
34-
|<img width=200 src="https://user-images.githubusercontent.com/48354989/205481015-7fdbfa1b-9dbe-4c3a-ba46-4e155e4f4178.png">|검색|- 원하는 장소를 검색할 수 있습니다.<br/>- 목적지에 갈 수 있는 대중교통 경로를 알려줍니다.<br/>|
35-
|<img width=200 src="https://user-images.githubusercontent.com/48354989/205481015-7fdbfa1b-9dbe-4c3a-ba46-4e155e4f4178.png">|막차|- 경로상에 있는 승차지의 **막차 시간**을 알려줍니다.<br/>- 승차지 사이의 이동 거리와 막차 시간을 고려하여, **첫 승차지에 탑승하러 출발해야 하는 찐-막차 시간**을 알려줍니다.<br/>|
36-
|<img width=200 src="https://user-images.githubusercontent.com/48354989/205481015-7fdbfa1b-9dbe-4c3a-ba46-4e155e4f4178.png">|알람|- 사용자가 원하는 경로의 막차 시간 00분 전에 알람을 설정할 수 있습니다.<br/>- 알람을 소리 또는 진동으로 선택할 수 있습니다.<br/>- 실시간으로 변동되는 막차 시간을 계산해서 알람을 알려줍니다.<br/>|
37-
|<img width=200 src="https://user-images.githubusercontent.com/48354989/205480897-3bb391fa-174e-4622-b16c-1a4817c74f78.png">|미션|- 사용자의 현재 **실시간 위치**를 보여줍니다.<br/>- **목적지까지의 경로**를 표시해줍니다.<br/>- 막차 시간보다 먼저 도착할지 **시합**할 수 있습니다.<br/>|
25+
### [앱 실행해보러 가기](https://github.com/boostcampwm-2022/android10-PlzStop/releases/tag/v1.0.0)
3826

3927
## 팀 소개 🧑‍🤝‍🧑
4028
| K008 김시진 | K037 이종성 | K039 이지민| K048 조경현|
4129
|:-----------:|:----------:|:----------:|:----------:|
4230
|<img src="https://user-images.githubusercontent.com/74500793/200560529-5c77f1a6-bcdc-4517-a13f-1f274683f530.png" width="150" height="150">|<img src="https://user-images.githubusercontent.com/74500793/200560658-e61ebec8-5e5d-42cf-9a65-a9f34bbebde7.png" width="150" height="150">|<img src="https://user-images.githubusercontent.com/74500793/200560030-6b96b399-e1c0-40d9-8901-2a959d437ab5.png" width="150" height="150">|<img src="https://user-images.githubusercontent.com/74500793/200560802-28af2528-a1e9-48cb-9e5e-889793bb53bb.png" width="150" height="150">|
4331
|[@koreatlwls](https://github.com/koreatlwls)| [@DoTheBestMayB](https://github.com/DoTheBestMayB) |[@jeeminimini](https://github.com/jeeminimini)|[@khcho226](https://github.com/khcho226)|
32+
33+
</br>
34+
35+
# 주요 기능
36+
37+
## 🗺️ 지도
38+
- 사용자의 현재 위치를 **실시간으로 트래킹**하여 보여줍니다.
39+
- 지도상의 임의의 지점을 클릭하면 **현 위치로부터의 거리를 포함한 상세 정보**를 보여줍니다.
40+
- 막차 알람이 설정되어 있으면 화면 하단에 **알람 정보**를 보여줍니다.
41+
42+
|지도|지도|
43+
|:------:|:-----:|
44+
| <img width="200" src="https://user-images.githubusercontent.com/61337202/207513688-f2beffbc-046c-4005-affe-69fe2f6120f0.gif"> | <img width="200" src="https://user-images.githubusercontent.com/61337202/207513754-5e50e63a-1b1d-49f6-9f4a-b94c784f2699.gif"> |
45+
46+
## 🔍 검색
47+
- 원하는 장소를 검색할 수 있습니다.
48+
- 목적지에 갈 수 있는 대중교통 경로를 알려줍니다.
49+
50+
|검색|검색|검색|
51+
|:------:|:-----:|:-----:|
52+
| <img width="200" src="https://user-images.githubusercontent.com/61337202/207515368-9e24608f-31f7-426b-80e0-7867f9da2e30.gif"> | <img width="200" src="https://user-images.githubusercontent.com/61337202/207515372-c589c3e5-538d-4090-a578-887ca149537e.gif"> | <img width="200" src="https://user-images.githubusercontent.com/61337202/207515376-35af8019-c240-46a9-b9a4-680fb83a6d37.gif"> |
53+
54+
## 🚌 막차
55+
- 경로상에 있는 승차지의 막차 시간을 알려줍니다.
56+
- 승차지 사이의 이동 거리와 막차 시간을 고려하여, 첫 승차지에 탑승하러 출발해야 하는 찐-막차 시간을 알려줍니다.
57+
58+
|막차|막차|
59+
|:------:|:-----:|
60+
| <img width="200" src="https://user-images.githubusercontent.com/61337202/207516539-b8f4fe17-f119-4bee-a3f4-266baaebbd13.gif"> | <img width="200" src="https://user-images.githubusercontent.com/61337202/207520444-4bbe104b-74da-474d-bb15-4326de3edc04.gif"> |
61+
62+
## ⏰ 알람
63+
- 사용자가 원하는 경로의 막차 시간 00분 전에 알람을 설정할 수 있습니다.
64+
- 알람을 소리 또는 진동으로 선택할 수 있습니다.
65+
66+
|알람|알람|
67+
|:------:|:-----:|
68+
| <img width="200" src="https://user-images.githubusercontent.com/61337202/207517640-86b73e4a-3b2e-4c2c-9b79-966e09264fc1.gif"> | <img width="200" src="https://user-images.githubusercontent.com/61337202/207517641-f73be4a2-db02-46ff-a1cb-0518388ce27e.gif"> |
69+
70+
## 🏃🏻 미션
71+
- 사용자의 현재 실시간 위치를 보여줍니다.
72+
- 목적지까지의 경로를 표시해줍니다.
73+
- 막차시간보다 먼저 도착할지 시합할 수 있습니다.
74+
75+
|미션|미션|
76+
|:------:|:-----:|
77+
| <img width="200" src="https://user-images.githubusercontent.com/61337202/207521798-682d24d8-197c-4e9a-ab02-bad3ec67f26c.gif"> | <img width="200" src="https://user-images.githubusercontent.com/61337202/207518265-273d3893-f0c9-424e-9b8e-0aaec3174b65.gif"> |
78+
79+
</br>
80+
81+
# 기술 스택
82+
83+
> Clean Architecture
84+
>
85+
- UseCase를 이용해 기능 직관적 판단 가능
86+
- 새로운 기능이 추가되거나 내부 로직이 변경되어야 할 때 유연하게 대처 가능
87+
88+
> Multi Module
89+
>
90+
- 수정된 모듈만 빌드 → 빌드 시간 단축
91+
- 의존성이 낮아질 수 있다.
92+
93+
> Hilt
94+
>
95+
- @AndroidEntryPoint를 사용하여 Service, BroadCastReceiver에도 의존성 주입 가능
96+
- 프로젝트 설정의 간소화
97+
- 쉬운 모듈 탐색과 통합
98+
99+
> Navigation
100+
>
101+
- Safe Args
102+
- Activity보다 가벼운 Fragment
103+
- 쉬운 화면 전환 Animation 추가
104+
105+
> `Moshi` vs Gson
106+
>
107+
- 직렬화 실패 메시지 제공
108+
- 다형성 데이터 직렬화 제공
109+
- Codegen 방식
110+
111+
> `T Map` vs 타 Map SDK
112+
>
113+
- T Map 대중교통 API와의 원활한 데이터 연동을 위해 사용
114+
- 벡터 맵(Vector Map) 지원
115+
- 다른 지도 어플에 비해 깔끔한 UI
116+
- 타 Map SDK에 비해 용량이 적음
117+
118+
> `DataStore` vs Sharedpreference
119+
>
120+
- DataStore는 코루틴과 Flow를 통해 읽고 쓰기에 대한 비동기 API를 제공
121+
- DataStore는 UI 쓰레드를 호출해도 안전
122+
- Runtime Exception으로부터 안전
123+
124+
> `Foreground Service` vs WorkManger
125+
>
126+
- 둘 다 즉시 실행해야하는 작업에 사용
127+
- WorkManager의 경우 상황에 따라 지연 가능
128+
- 사용자의 경로를 지속적으로 보여주며 UI를 변경해야하기 때문에 Foreground Service 사용
129+

data/src/main/java/com/stop/data/di/NetworkModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import okhttp3.Response
2020
import okhttp3.logging.HttpLoggingInterceptor
2121
import retrofit2.Retrofit
2222
import retrofit2.converter.moshi.MoshiConverterFactory
23+
import java.util.concurrent.TimeUnit
2324
import javax.inject.Named
2425
import javax.inject.Singleton
2526

@@ -40,6 +41,9 @@ internal object NetworkModule {
4041
loggingInterceptor: HttpLoggingInterceptor,
4142
): OkHttpClient {
4243
return OkHttpClient.Builder()
44+
.connectTimeout(5, TimeUnit.SECONDS)
45+
.writeTimeout(5, TimeUnit.SECONDS)
46+
.readTimeout(5, TimeUnit.SECONDS)
4347
.addInterceptor(loggingInterceptor)
4448
.addInterceptor(customInterceptor)
4549
.build()

domain/src/main/java/com/stop/domain/usecase/route/GetLastTransportTimeUseCaseImpl.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ internal class GetLastTransportTimeUseCaseImpl @Inject constructor(
378378
lastTime += TIME_CORRECTION_VALUE
379379
}
380380

381-
val lastTimeString = lastTime.toString().chunked(2).joinToString(":")
381+
val lastTimeString = lastTime.toString().padStart(6, '0').chunked(2).joinToString(":")
382382

383383
return TransportLastTime(
384384
transportMoveType = TransportMoveType.BUS,
@@ -521,7 +521,7 @@ internal class GetLastTransportTimeUseCaseImpl @Inject constructor(
521521
lastTime += TIME_CORRECTION_VALUE
522522
}
523523

524-
val lastTimeString = lastTime.toString().chunked(2).joinToString(":")
524+
val lastTimeString = lastTime.toString().padStart(6, '0').chunked(2).joinToString(":")
525525

526526
return TransportLastTime(
527527
transportMoveType = TransportMoveType.BUS,

presentation/src/main/java/com/stop/model/ErrorType.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ enum class ErrorType(val stringResourcesId: Int) {
1212
AVAILABLE_BUS_NO_EXIST_YET(R.string.available_bus_no_exist_yet),
1313
BUS_DISAPPEAR_SUDDENLY(R.string.bus_disappear_suddenly),
1414
MISSION_SOMETHING_WRONG(R.string.mission_something_wrong),
15+
SOCKET_TIMEOUT_EXCEPTION(R.string.socket_timeout_exception_please_retry),
16+
UNKNOWN_EXCEPTION(R.string.unknown_exception_occur),
17+
UNKNOWN_HOST_EXCEPTION(R.string.unknown_host_exception_occur),
1518
}

presentation/src/main/java/com/stop/ui/alarmsetting/AlarmSettingFragment.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ class AlarmSettingFragment : Fragment() {
111111
//alarmSettingViewModel.makeAlarmWorker(transportLastTime.timeToBoard)
112112

113113
val navController = findNavController()
114-
navController.setGraph(R.navigation.nav_graph)
115-
navController.popBackStack(R.id.action_global_mapFragment, false)
114+
navController.popBackStack(R.id.mapFragment, false)
116115
requireActivity().viewModelStore.clear()
117116
}
118117

presentation/src/main/java/com/stop/ui/alarmsetting/AlarmSettingViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class AlarmSettingViewModel @Inject constructor(
6262
_alarmItem.value = it
6363

6464
if (it != null) {
65-
when(missionStatus){
65+
when (missionStatus) {
6666
MissionStatus.BEFORE -> {
6767
alarmStatus.value = AlarmStatus.EXIST
6868
}
@@ -87,6 +87,7 @@ class AlarmSettingViewModel @Inject constructor(
8787
}
8888

8989
fun callAlarm(time: String) {
90+
deleteAlarm()
9091
alarmFunctions.callAlarm(time, alarmTime.value ?: 0)
9192
}
9293

presentation/src/main/java/com/stop/ui/map/MapFragment.kt

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.view.LayoutInflater
1111
import android.view.View
1212
import android.view.ViewGroup
1313
import androidx.activity.result.contract.ActivityResultContracts
14+
import androidx.core.os.bundleOf
1415
import androidx.fragment.app.Fragment
1516
import androidx.fragment.app.activityViewModels
1617
import androidx.fragment.app.viewModels
@@ -22,7 +23,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
2223
import com.skt.tmap.TMapPoint
2324
import com.stop.AlarmActivity
2425
import com.stop.R
25-
import com.stop.RouteNavGraphDirections
2626
import com.stop.alarm.SoundService
2727
import com.stop.databinding.FragmentMapBinding
2828
import com.stop.model.AlarmStatus
@@ -141,24 +141,14 @@ class MapFragment : Fragment(), MapHandler {
141141

142142
binding.homePanel.viewPanelStart.setOnClickListener {
143143
placeSearchViewModel.setPanelVisibility(View.INVISIBLE)
144-
findNavController().apply {
145-
setGraph(R.navigation.route_nav_graph)
146-
navigate(
147-
RouteNavGraphDirections.actionGlobalRouteFragment()
148-
.setStart(placeSearchViewModel.panelInfo)
149-
)
150-
}
144+
val bundle = bundleOf("start" to placeSearchViewModel.panelInfo)
145+
findNavController().navigate(R.id.action_mapFragment_to_route_nav_graph, bundle)
151146
}
152147

153148
binding.homePanel.viewPanelEnd.setOnClickListener {
154149
placeSearchViewModel.setPanelVisibility(View.INVISIBLE)
155-
findNavController().apply {
156-
setGraph(R.navigation.route_nav_graph)
157-
navigate(
158-
RouteNavGraphDirections.actionGlobalRouteFragment()
159-
.setEnd(placeSearchViewModel.panelInfo)
160-
)
161-
}
150+
val bundle = bundleOf("end" to placeSearchViewModel.panelInfo)
151+
findNavController().navigate(R.id.action_mapFragment_to_route_nav_graph, bundle)
162152
}
163153
}
164154

presentation/src/main/java/com/stop/ui/route/RouteFragment.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,7 @@ class RouteFragment : Fragment() {
5555

5656
backPressedCallback = object : OnBackPressedCallback(true) {
5757
override fun handleOnBackPressed() {
58-
val navController = findNavController()
59-
navController.setGraph(R.navigation.nav_graph)
60-
navController.popBackStack(R.id.action_global_mapFragment, false)
58+
findNavController().popBackStack(R.id.mapFragment, false)
6159
}
6260
}
6361
requireActivity().onBackPressedDispatcher.addCallback(this, backPressedCallback)
@@ -163,7 +161,7 @@ class RouteFragment : Fragment() {
163161
requireArguments().clear()
164162

165163
if (args?.start != null || args?.end != null) {
166-
routeViewModel.getRoute()
164+
routeViewModel.patchRoute()
167165
}
168166
}
169167

@@ -176,9 +174,9 @@ class RouteFragment : Fragment() {
176174

177175
val dialogView = layoutInflater.inflate(R.layout.dialog_progress, null)
178176
alertDialog = AlertDialog.Builder(requireContext())
179-
.setView(dialogView)
180-
.setCancelable(false)
181-
.create()
177+
.setView(dialogView)
178+
.setCancelable(false)
179+
.create()
182180
alertDialog.window?.setBackgroundDrawableResource(R.color.transparent)
183181
routeViewModel.alertDialog = alertDialog
184182
}

presentation/src/main/java/com/stop/ui/route/RouteViewModel.kt

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import com.stop.model.ErrorType
1414
import com.stop.model.Event
1515
import com.stop.model.route.Place
1616
import dagger.hilt.android.lifecycle.HiltViewModel
17+
import kotlinx.coroutines.CoroutineExceptionHandler
18+
import kotlinx.coroutines.Dispatchers
1719
import kotlinx.coroutines.launch
20+
import java.net.SocketTimeoutException
21+
import java.net.UnknownHostException
1822
import javax.inject.Inject
1923

2024
@HiltViewModel
@@ -50,7 +54,17 @@ class RouteViewModel @Inject constructor(
5054
val isLoading: LiveData<Event<Boolean>>
5155
get() = _isLoading
5256

53-
fun getRoute(isShowError: Boolean = true) {
57+
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
58+
val errorMessage = when (throwable) {
59+
is SocketTimeoutException -> Event(ErrorType.SOCKET_TIMEOUT_EXCEPTION)
60+
is UnknownHostException -> Event(ErrorType.UNKNOWN_HOST_EXCEPTION)
61+
else -> Event(ErrorType.UNKNOWN_EXCEPTION)
62+
}
63+
_errorMessage.postValue(errorMessage)
64+
_isLoading.postValue(Event(false))
65+
}
66+
67+
fun patchRoute(isShowError: Boolean = true) {
5468
val originValue = _origin.value ?: let {
5569
if (!isShowError) {
5670
return
@@ -75,30 +89,30 @@ class RouteViewModel @Inject constructor(
7589
endY = destinationValue.coordinate.latitude,
7690
)
7791

78-
viewModelScope.launch {
92+
viewModelScope.launch(Dispatchers.Default + coroutineExceptionHandler) {
7993
val itineraries = getRouteUseCase(routeRequest)
8094
if (itineraries.isEmpty()) {
81-
_errorMessage.value = Event(ErrorType.NO_ROUTE_RESULT)
82-
_routeResponse.value = listOf()
83-
_isLoading.value = Event(false)
95+
_errorMessage.postValue(Event(ErrorType.NO_ROUTE_RESULT))
96+
_routeResponse.postValue(listOf())
97+
_isLoading.postValue(Event(false))
8498
return@launch
8599
}
86-
this@RouteViewModel._routeResponse.value = itineraries
87-
_isLoading.value = Event(false)
100+
this@RouteViewModel._routeResponse.postValue(itineraries)
101+
_isLoading.postValue(Event(false))
88102
}
89103
}
90104

91105
fun changeOriginAndDestination() {
92106
_origin.value = _destination.value.also {
93107
_destination.value = _origin.value
94108
}
95-
getRoute(false)
109+
patchRoute(false)
96110
}
97111

98112
fun calculateLastTransportTime(itinerary: Itinerary) {
99113
checkClickedItinerary(itinerary)
100-
viewModelScope.launch {
101-
this@RouteViewModel._lastTimeResponse.value = Event(getLastTransportTimeUseCase(itinerary) )
114+
viewModelScope.launch(Dispatchers.Default + coroutineExceptionHandler) {
115+
this@RouteViewModel._lastTimeResponse.postValue(Event(getLastTransportTimeUseCase(itinerary)))
102116
}
103117
}
104118

presentation/src/main/java/com/stop/ui/routedetail/RouteDetailFragment.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import android.view.LayoutInflater
66
import android.view.View
77
import android.view.ViewGroup
88
import androidx.core.view.GravityCompat
9-
import androidx.navigation.findNavController
109
import androidx.navigation.fragment.findNavController
1110
import com.stop.R
1211
import androidx.navigation.navGraphViewModels
@@ -76,8 +75,7 @@ class RouteDetailFragment : Fragment(), RouteDetailHandler {
7675

7776
binding.imageViewClose.setOnClickListener {
7877
findNavController().apply {
79-
setGraph(R.navigation.nav_graph)
80-
popBackStack(R.id.action_global_mapFragment, false)
78+
popBackStack(R.id.mapFragment, false)
8179
requireActivity().viewModelStore.clear()
8280
}
8381
}

0 commit comments

Comments
 (0)