티스토리 뷰
3-2. 지하철
1) 사용한 Api
열린데이터광장 메인
데이터분류,데이터검색,데이터활용
data.seoul.go.kr
서울특별시에서 제공하는 [서울시 지하철 실시간 도착정보] api 사용하였습니다.
열린데이터광장 메인
데이터분류,데이터검색,데이터활용
data.seoul.go.kr
서울 교통공사에서 제공하는 [서울시 역 코드로 지하철역별 열차 시간표 정보 검색] api를 이용하였습니다.
서울교통공사_서울 도시철도 목적지 경로정보
수도권 전체 열차 경로 탐색 데이터를 출발역, 도착역, 출발시간, 도착시간, 소요시간, 환승 횟수, 경로 역사 코드 등의 항목으로 제공하는 서비스
www.data.go.kr
서울 교통공사에서 제공하는 [서울교통공사_서울 도시철도 목적지 경로 정보] api를 이용하였습니다.
하얀색 표는 서울특별시에 제공하는 실시간 도착 정보를 제공하는 api에서 사용하는 코드이고
회색 표는 서울교통공사에서 제공하는 api에서 사용하는 코드입니다.
하얀색 표의 정보는 위의 링크에서 받을 수 있고 회색 표의 경우 다음 링크에서 받을 수 있습니다.
열린데이터광장 메인
데이터분류,데이터검색,데이터활용
data.seoul.go.kr
두 개의 코드를 같이 사용하기 위한 고민

의정부역을 기준으로 두 개의 키를 조합해 보았습니다.
앞 4자리는 호선 코드를, 뒤에 6자리는 외부 코드 및 여백으로 0이 들어갔다고 생각됩니다.

강남구청역을 기준으로 두 개의 키를 조합해 보았습니다.
역시 앞 4자리는 호선 코드입니다.
뒤 6자리의 경우 강남구청의 외부 코드는 K213입니다.
K를 아스키코드로 변환하면 75가 되고 뒤의 213가 조합하면 1075075213을 만들 수 있습니다.

신설동역을 기준으로 두 개의 키를 조합해 보았습니다.
역시 앞의 4자리는 호선 코드로 동일합니다.
근데 뒤에 6자리의 경우 역 코드를 이용해서 조합이 된 것을 확인할 수 있습니다.
아스키코드로 변환해서 사용하는 것 까지는 어떻게든 할 수 있을 텐데 외부 코드를 안 쓰고 역 코드를 쓴다고 하면 또 예외 처리를 해야합니다.
특정 노선만 저렇게 처리가 된 것 같아서 불가능한 것은 아닌데 다른 예외가 있을 가능성이 있기 때문에 안심하고 사용할 수는 없었습니다.
그렇다고 역 이름을 가지고 조합을 할 수도 없습니다.
쌍용(나사렛대) -> 쌍용 서울 -> 서울역
대표적인 예시로 두 개의 테이블에서 서로 다른 이름을 사용하고 있습니다.
또한 같은 역이름이라고 해도 여러 호선이 있을 경우 또 다른 처리를 해주어야 합니다.
제공하는 정보만 가지고 어떻게든 해보려고 하였으나 마땅한 방법이 없어서 직접 파일을 만들어서 사용하기로 결정하였습니다.
SUBWAY_ID,SUBWAY_NAME,STATN_ID,STATN_NM,STATN_CODE,OUTER_CODE
1001,1호선,1001000100,소요산,1916,100
1001,1호선,1001000101,동두천,1915,101
1001,1호선,1001000102,보산,1914,102
1001,1호선,1001000103,동두천중앙,1913,103
1001,1호선,1001000104,지행,1912,104
1001,1호선,1001000105,덕정,1911,105
...
csv 파일로 다음과 같이 조합을 하여서 만들었습니다.
해당 파일을 불러와서 데이터베이스에 저장 후 사용하였습니다.
2) 데이터베이스
/**
* 지하철 역 정보
* @param stationCode 서울교통공사에서 제공하는 API에서 사용하는 코드
* @param stationId 서울특별시에서 제공하는 API에서 사용하는 코드
* @param outerCode 서울교통공사에서 제공하는 API에서 사용하는 외부 코드
* @param stationName 지하철 역 이름
* @param lineNum 호선 번호
* @param lineName 호선 이름
* **/
@Entity
data class StationEntity(
@PrimaryKey
val stationCode: String,
val stationId: String,
val outerCode: String,
val stationName: String,
val lineNum: String,
val lineName: String,
)
위에서 만든 csv의 정보들을 담을 데이터베이스 Entity입니다.
/**
* 즐겨 찾기
* @param type Bus, BusStop, Subway, SubwayDestination 중 1개 선택 (const 제공)
* @param startTime 시작 시간
* @param endTime 종료 시간
* @param name 이름
* @param id 서버 조회를 위한 아이디
* @param timeStamp 등록 시간
* **/
@Entity(indices = [
Index(unique = true, value = ["id", "name"])
])
data class FavoriteEntity(
@PrimaryKey(autoGenerate = true) val index: Int,
val type: String,
val startTime: String,
val endTime: String,
val name: String,
val id: String,
val timeStamp: Long
) {
companion object {
const val TypeBus = "Bus"
const val TypeBusStop = "BusStop"
const val TypeSubway = "Subway"
const val TypeSubwayDestination = "SubwayDestination"
const val Separator = "|||"
}
}
즐겨찾기 정보를 담는 데이터베이스 Entity입니다.
3) 지하철역 검색
이 화면에서는 지하철역 리스트 형식으로 화면에 나타납니다.
리스트의 아이템을 선택 시 지하철 도착 정보를 바텀 시트로 보여줍니다.
출발 또는 도착을 선택 시 상단 카드가 활성화되고 ‘>’을 클릭하여 목적지 경로를 조회 페이지로 이동합니다.
검색을 통해 특정 문구가 들어간 지하철역 조회가 가능하며 즐겨찾기만 표시할 수 있습니다.
override suspend fun fetchStationItems(
stationName: String,
isFavoriteOnly: Boolean,
) = flow {
val allOrFavorite = if (isFavoriteOnly) listOf(1) else listOf(0, 1)
if (subwayDao.getNumberOfStations() <= 0) {
insertSubwayStation()
}
subwayDao.fetchStationItems(stationName, allOrFavorite).collect {
emit(it)
}
}
private suspend fun insertSubwayStation() {
try {
val inputStream = context.resources.openRawResource(R.raw.station_info)
val reader = BufferedReader(InputStreamReader(inputStream))
val stationInfoList = mutableListOf<StationEntity>()
reader.forEachLine {
val infoList = it.split(',')
stationInfoList.add(StationEntity.fromCsv(infoList = infoList))
}
subwayDao.insertSubwayStation(stationInfoList.drop(1))
} catch (e: IOException) {
e.printStackTrace()
} catch (e: NotFoundException) {
e.printStackTrace()
}
}
Repository에서 subwayDao.getNumberOfStations()를 통해 지하철역 정보가 데이터베이스에 있는지 확인합니다.
없다고 하면 insertSubwayStation()를 통해 csv 파일을 읽어와서 데이터베이스에 내용을 채워줍니다.
데이터가 있다고 하면 다음의 쿼리를 통해 데이터를 읽어옵니다.
@Query("SELECT stationCode, stationId, stationName, group_concat(lineNum, ',') lineNames, CASE WHEN Favorite.id IS NULL THEN 0 ELSE 1 END AS isFavorite\n" +
"FROM StationEntity as Station\n" +
"LEFT JOIN FavoriteEntity as Favorite\n" +
"ON Station.stationCode = Favorite.id\n" +
"WHERE stationName LIKE '%' || :stationName || '%' AND isFavorite IN (:allOrFavorite)\n" +
"GROUP BY stationName\n" +
"ORDER BY isFavorite DESC, stationName ASC")
fun fetchStationItems(
stationName: String = "",
allOrFavorite: List<Int>,
): Flow<List<StationItem>>
이번 쿼리는 화면 요구사항이 있어서 좀 복잡하게 되었습니다.
- FavoriteEtity를 LEFT JOIN 하여 즐겨찾기 정보와 역의 정보를 id를 기준으로 연결합니다.
CASE WHEN Favorite.id IS NULL THEN 0 ELSE 1 END AS isFavorite
CASE를 사용하여 즐겨찾기가 있을 경우 1(true)를, 없을 경우 0(false)를 반환합니다. - GROUP BY stationName를 이용하여 지하철역으로 그룹화합니다.
ex) 도봉산의 경우 1호선과 7호선이 있는데 이를 2개의 Row가 아닌 하나의 Row로 만듭니다.
그룹화한 뒤 각 호선 정보에 대해서는 유지를 해야 하기 때문에
group_concat(lineNum, ',') lineNames를 하여 ‘1호선, 7호선’ 이렇게 나오도록 설정하였습니다. - WHERE 조건을 통해 역 이름이나, 즐겨찾기만 출력 여부에 대해 설정할 수 있도록 하였습니다.
- 정렬 기준은 즐겨찾기를 내림차순으로 하고, 역이름을 오름차순으로 설정하였습니다.
val stationItems = stationItemsSharedFlow
.flatMapLatest {
repository.fetchStationItems(
stationName = it,
isFavoriteOnly = isFavoriteOnly
)
}
.onStart { _isProgress.value = true }
.onCompletion { _isProgress.value = false }
.retryWhen { cause, attempt ->
if (cause is NoSuchElementException && attempt < 2) {
true
} else {
throw cause
}
}
.catch { it.printStackTrace() }
읽어온 데이터는 위와 같이 ViewModel에서 불러와서 화면에 출력합니다.
이번에는 retryWhen이라는 것을 사용해 보았습니다.
예외사항이 발생했을 때 컨트롤할 수 있는 기능인데
cuase를 통해 예외 발생 원인을, attempt를 통해 재시도 횟수의 정보를 받을 수 있습니다.
혹시라도 일시적 오류로 인해 데이터가 나오지 않을 경우를 대비하여 1번 더 재시도를 할 수 있도록 설정해 보았습니다.
subwayLineId 지하철호선ID
updnLine 상하행선구분 (2호선 : (내선:0,외선:1),상행,하행)
prevStationId 이전지하철역ID
nextStationId 다음지하철역ID
destinationName 종착지하철역명
stationId 지하철 역 ID
subwayType 열차 종류 (급행, ITX)
arrTime 열차도착예정시간 (단위:초)
[서울시 지하철 실시간 도착정보] api에서 위의 데이터 + a를 한 묶음으로 해서 리스트로 데이터가 넘어옵니다.
/**
* @param subwayLineId 지하철 호선 아이디
* @param stationCode 지하철 역 코드
* @param currentStationName 현재 역 이름
* @param prevStationName 이전 역 이름
* @param nextStationName 다음 역 이름
* @param arrItemList 지하철 도착 정보
* **/
data class SubwayArrival(
val subwayLineId: String,
val stationCode: String,
val currentStationName: String,
val prevStationName: String,
val nextStationName: String,
val arrItemList: List<SubwayArrivalItem>
)
/**
* @param arrInfo 도착 정보 : 00행 0000
* @param isUpLine 상행 또는 내선 여부
* **/
data class SubwayArrivalItem(
val arrInfo: String,
val isUpLine: Boolean
)
저는 제 화면에 맞게 위와 같은 형식으로 데이터를 가공하려고 합니다.
override fun fetchRealtimeStationArrivals(
keyword: String
) = flow {
val items = mutableListOf<SubwayArrival>()
val result = client.fetchRealtimeStationArrivals(keyword)
if (result.isEmpty()) throw NoSuchElementException("데이터가 없습니다.")
items.addAll(
result
.groupBy { it.subwayLineId }
.map { (subwayLineId, subwayArrivalInfoList) ->
createSubwayArrival(
stationName = result[0].stationName,
subwayLineId = subwayLineId,
subwayArrivalInfoList = subwayArrivalInfoList
)
}
)
emit(items.sortedBy { it.subwayLineId })
}
private suspend fun createSubwayArrival(
stationName: String,
subwayLineId: String,
subwayArrivalInfoList: List<SubwayArrivalInfo>
): SubwayArrival {
val stationCode = fetchSubwayCodeById(subwayArrivalInfoList[0].stationId)
val prevStation =
subwayDao.fetchSubwayNameById(subwayArrivalInfoList[0].nextStationId)
val nextStation =
subwayDao.fetchSubwayNameById(subwayArrivalInfoList[0].prevStationId)
return SubwayArrival(
subwayLineId = subwayLineId,
stationCode = stationCode,
prevStationName = prevStation,
nextStationName = nextStation,
currentStationName = stationName,
arrItemList = subwayArrivalInfoList
.map {
SubwayArrivalItem(
arrInfo = "${it.destinationName}행 ${it.arrInfo}",
isUpLine = it.updnLine == "상행" || it.updnLine == "외선"
)
}
)
}
실시간 정보를 불러오면 호선, 상행선/하행선 또는 내선/외선이 구분 없이 위의 데이터들이 묶여서 내려옵니다.
그렇기 때문에 받아온 결과 리스트에 groupBy를 이용하여 호선 단위로 그룹화합니다.
그룹화한 데이터에서 map을 원하는 데이터 클래스 형식으로 가공합니다.
private val _arrivalInfoList = MutableStateFlow<List<SubwayArrival>>(listOf())
val arrivalInfoList: Flow<List<SubwayArrival>> = _arrivalInfoList
/** 지하철 도착 정보 조회 **/
fun fetchRealtimeStationArrivals(
stationName: String
) {
repository
.fetchRealtimeStationArrivals(stationName)
.onStart { _isProgress.value = true }
.onEach { _arrivalInfoList.value = it }
.onCompletion { _isProgress.value = false }
.catch {
_arrivalInfoList.value = emptyList()
it.printStackTrace()
}
.launchIn(viewModelScope)
}
도착 정보는 역을 선택할 때마다 계속 데이터가 변경되어야 하는 부분이어서 기존에 사용하던 방법으로 표현을 해 보았습니다.
4) 지하철 목적지 경로 조회
이 화면에서는 선택한 출발지와 목적지의 경로를 보여줍니다.
평일/토요일/휴일 중 1개와 시간을 선택하여 최적의 경로를 조회합니다.
override fun fetchSubwayRoute(
searchTime: String,
startStationCode: String,
endStationCode: String,
week: String
): Flow<SubwayRouteInfo> = flow {
val list = mutableListOf<RouteItem>()
val numberOfRow = 50
var pageNumber = 1
var totalCount: Int? = null
var arrivalTime = ""
var fee = 0
var transferCount = 0
try {
while (totalCount == null || (pageNumber - 1) * numberOfRow < totalCount) {
val result = client.fetchSubwayRoute(
pageNo = pageNumber,
numOfRows = numberOfRow,
searchTime = searchTime,
startStationCode = startStationCode,
endStationCode = endStationCode,
week = week
)
arrivalTime = result.data.arrivalTime
fee = result.data.fee
list.addAll(result.data.route.map { it.toRouteItem() })
totalCount = result.totalCount
transferCount = result.data.transfer
pageNumber++
}
} catch (e: Exception) {
e.printStackTrace()
}
emit(
SubwayRouteInfo(
deptTime = list.getOrNull(0)?.time ?: "",
arrivalTime = formatTime(arrivalTime),
transferCount = transferCount,
fee = fee.priceFormat(),
list = list
)
)
}
[서울교통공사_서울 도시철도 목적지 경로 정보] api를 사용하여 목적지 경로를 조회합니다.
해당 api는 버스 노선과 마찬가지로 반복을 통해서 데이터를 가져오는 방식의 api입니다.
버스 노선의 경우 다음 화면에서 사용하는 방식과 동일하게 수정을 할 예정입니다.
그런데 이번의 경우에는 데이터의 구조가 조금 달라서 어떻게 하면 깔끔하게 할지 고민을 하고 있습니다.
5) 지하철 시간표 조회
이 화면에서는 지하철 시간표를 보여줍니다.
평일/토요일/공휴일 중 하나를 선택해서 시간표를 볼 수 있습니다.
override fun fetchSubwaySchedule(
stationCode: String,
week: Int,
direction: Int
) = flow {
var start = 1
val numberOfRow = 100
var totalCount: Int? = null
try {
while (totalCount == null || start < totalCount) {
val result = client.fetchSubwaySchedule(
start = start,
end = start + numberOfRow - 1,
stationCode = stationCode,
week = week,
direction = direction
)
totalCount = result.totalCount
start += numberOfRow
emit(result.row.map { it.toSubwayScheduleInfo() })
}
} catch (e: Exception) {
e.printStackTrace()
emit(emptyList())
}
}
[서울시 역 코드로 지하철역별 열차 시간표 정보 검색] api를 통하여 시간표를 조회합니다.
이 api는 상행 또는 내선으로 1번부터 끝까지 조회하고 하행 또는 외선으로 1번부터 끝까지 조회하여서 화면에 출력해야 합니다.
api에서 결과를 받을때 마다 emit 하여 데이터를 전달합니다.
private fun fetchSubwaySchedules() = viewModelScope.launch {
originList.clear()
fetchSubwaySchedule(1)
fetchSubwaySchedule(2)
}
private suspend fun fetchSubwaySchedule(direction: Int) = repository.fetchSubwaySchedule(
stationCode = stationCode,
week = _week.value,
direction = direction
).collect {
originList.addAll(it)
}
상행/하행 또는 내선/외선을 각각 호출해 주고 결과를 collect를 통해 originList에 저장해 주었습니다.
// 원본 리스트
private val originList = mutableStateListOf<SubwayScheduleInfo>()
// 지하철 시간표 리스트
val scheduleList: List<Pair<String, List<SubwayScheduleInfo>>>
get() {
return originList
.sortedWith(compareBy({ info -> info.hour }, { info -> info.minute }))
.groupBy { it.hour }
.toList()
}
저장된 원본 리스트를 화면에서 사용할 때에는 get()을 통해 변형된 리스트를 사용하도록 하였습니다.
- 전체 리스트를 시간, 분으로 정렬
- 시간 단위로 그룹화
- 리스트로 변경
originList가 데이터가 추가되면 scheduleList는 그에 맞게 리스트의 내용이 수정됩니다.
이렇게 해서 교통 관련 화면은 1단계 완료하였습니다.
홈 화면에 노출시키기 위해 시간 설정하는 화면은 아직 홈 화면을 어떻게 할지 정하진 못해서 정해 진 후에 진행할 예정입니다.
그 외 추가 기능이나 테스트 진행은 좀 더 프로젝트를 진행 후에 진행하려고 합니다.
'안드로이드 > 코드' 카테고리의 다른 글
Mj App : 게임 - 엘소드 (0) | 2023.06.27 |
---|---|
Mj App : 공통 (0) | 2023.06.27 |
Compose 내 관리앱 만들기 : 2. 교통 - 버스 (0) | 2023.03.19 |
Compose 내 관리앱 만들기 : 1. 프로젝트 구성 (0) | 2023.03.19 |
코드 분석 (0) | 2023.01.24 |
- Total
- Today
- Yesterday
- Retrofit
- Kotlin
- LazyColumn
- Compose BottomSheet
- Android Compose
- Compose Naver Map
- Compose MotionLayout
- Row
- Fast api
- Pokedex
- Compose QRCode Scanner
- Compose 네이버 지도
- Duplicate class found error
- 웹뷰
- Android
- 안드로이드 구글 지도
- Compose BottomSheetDialog
- column
- Compose ModalBottomSheetLayout
- compose
- 포켓몬 도감
- Gradient
- 안드로이드
- WorkManager
- Compose 네이버 지도 api
- WebView
- Compose ConstraintLayout
- Compose BottomSheetScaffold
- Duplicate class fond 에러
- Worker
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |